Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedMore than 25% of the files skipped due to max files limit. The review is being skipped to prevent a low-quality review. 74 files out of 182 files are above the max files limit of 100. Please upgrade to Pro plan to get higher limits. You can disable this status message by setting the Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds Catalogs pages and a client-side Product editor, refactors Payments UI (interval picker, dialogs, lists), safer deletion via typedEntries/typedFromEntries, email-queue env flags and boolean env helper, CI wait-port updates, new e2e tests, and several new components/exports (EditableGrid, RepeatingInput, PricingSection, PriceEditDialog). Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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. Comment |
56d73e2 to
726286c
Compare
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Greptile OverviewGreptile SummaryThis PR implements a comprehensive redesign of the payments products management UI with improved user experience and visual design. Key Changes
Issues Found
Design QualityThe redesign significantly improves the UX with better visual hierarchy, inline editing, and intuitive workflows. The code is well-structured with good separation of concerns. Confidence Score: 3/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant User
participant PageClient
participant CatalogView
participant ProductCard
participant ProductDialog
participant ItemDialog
participant Project
User->>PageClient: Opens payments page
alt No products/items exist
PageClient->>User: Shows welcome screen
User->>PageClient: Clicks "Create First Product"
PageClient->>CatalogView: Creates draft product
else Products exist
PageClient->>CatalogView: Shows catalog view (default)
Note over PageClient,CatalogView: User can toggle between<br/>catalog view and list view
end
User->>ProductCard: Clicks "Add product"
ProductCard->>ProductCard: Creates draft in edit mode
User->>ProductCard: Edits product details
User->>ProductCard: Adds prices and items
User->>ProductCard: Clicks "Save"
ProductCard->>Project: updateConfig(product)
Project-->>ProductCard: Success
ProductCard->>CatalogView: Updates display
User->>ProductCard: Clicks "Add Item"
ProductCard->>ItemDialog: Opens create dialog
User->>ItemDialog: Enters item details
ItemDialog->>Project: updateConfig(item)
Project-->>ItemDialog: Success
ItemDialog->>ProductCard: Returns new item ID
ProductCard->>ProductCard: Auto-selects new item
User->>ProductCard: Hovers over product
ProductCard->>CatalogView: Shows connections to items
User->>ProductCard: Clicks delete
ProductCard->>Project: Deletes product & empty catalog
Project-->>ProductCard: Success
|
| size="sm" | ||
| className="h-9 rounded-lg px-5 bg-foreground text-background hover:bg-foreground/90" | ||
| disabled={!canSaveProduct} | ||
| onClick={async () => { await handleSaveEdit(); }} |
There was a problem hiding this comment.
logic: Missing error handling for async operation - should use runAsynchronouslyWithAlert from @stackframe/stack-shared/dist/utils/promises instead of inline async handler
| onClick={async () => { await handleSaveEdit(); }} | |
| onClick={() => runAsynchronouslyWithAlert(async () => { await handleSaveEdit(); })} |
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/page-client-catalogs-view.tsx
Line: 1198:1198
Comment:
**logic:** Missing error handling for async operation - should use `runAsynchronouslyWithAlert` from `@stackframe/stack-shared/dist/utils/promises` instead of inline async handler
```suggestion
onClick={() => runAsynchronouslyWithAlert(async () => { await handleSaveEdit(); })}
```
**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.| onClick: async () => { | ||
| await onDelete(id); | ||
| setShowDeleteDialog(false); | ||
| setIsEditing(false); |
There was a problem hiding this comment.
logic: Missing error handling for async operation - use runAsynchronouslyWithAlert
| onClick: async () => { | |
| await onDelete(id); | |
| setShowDeleteDialog(false); | |
| setIsEditing(false); | |
| onClick: () => runAsynchronouslyWithAlert(async () => { | |
| await onDelete(id); | |
| setIsEditing(false); | |
| }) |
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/page-client-catalogs-view.tsx
Line: 1341:1343
Comment:
**logic:** Missing error handling for async operation - use `runAsynchronouslyWithAlert`
```suggestion
onClick: () => runAsynchronouslyWithAlert(async () => {
await onDelete(id);
setIsEditing(false);
})
```
**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.| onClick: async () => { | ||
| if (!productToDelete) return; | ||
| const config = project.useConfig(); | ||
| const updatedProducts = typedFromEntries( | ||
| typedEntries(config.payments.products) | ||
| .filter(([productId]) => productId !== productToDelete.id) | ||
| ); | ||
| await project.updateConfig({ "payments.products": updatedProducts }); | ||
| toast({ title: "Product deleted" }); |
There was a problem hiding this comment.
logic: Missing error handling - use runAsynchronouslyWithAlert for async onClick handler
| onClick: async () => { | |
| if (!productToDelete) return; | |
| const config = project.useConfig(); | |
| const updatedProducts = typedFromEntries( | |
| typedEntries(config.payments.products) | |
| .filter(([productId]) => productId !== productToDelete.id) | |
| ); | |
| await project.updateConfig({ "payments.products": updatedProducts }); | |
| toast({ title: "Product deleted" }); | |
| onClick: () => runAsynchronouslyWithAlert(async () => { | |
| if (!productToDelete) return; | |
| const config = project.useConfig(); | |
| const updatedProducts = typedFromEntries( | |
| typedEntries(config.payments.products) | |
| .filter(([productId]) => productId !== productToDelete.id) | |
| ); | |
| await project.updateConfig({ "payments.products": updatedProducts }); | |
| toast({ title: "Product deleted" }); | |
| setProductToDelete(null); | |
| }) |
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/page-client-list-view.tsx
Line: 443:451
Comment:
**logic:** Missing error handling - use `runAsynchronouslyWithAlert` for async onClick handler
```suggestion
onClick: () => runAsynchronouslyWithAlert(async () => {
if (!productToDelete) return;
const config = project.useConfig();
const updatedProducts = typedFromEntries(
typedEntries(config.payments.products)
.filter(([productId]) => productId !== productToDelete.id)
);
await project.updateConfig({ "payments.products": updatedProducts });
toast({ title: "Product deleted" });
setProductToDelete(null);
})
```
**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.
.../src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive visual redesign of the payments interface with improved styling, better user experience, and several bug fixes. The changes modernize the UI with updated color schemes, improved spacing, rounded corners, and enhanced visual feedback while also addressing data handling issues.
Key Changes:
- Redesigned UI components with modern styling (rounded corners, improved shadows, better color contrast)
- Fixed async state management bugs in price editing
- Improved product/item deletion logic to properly rebuild config objects
- Enhanced form validation and error messaging
- Added comprehensive code generation prompts for AI assistance
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| dropdown-menu.tsx | Updated spacing/padding, added icon prop support for menu items |
| item-dialog.tsx | Redesigned dialog with improved styling, fixed input validation to allow underscores |
| payment-product-table.tsx | Fixed product deletion to rebuild config object instead of setting to null |
| code-block.tsx | Added overflow handling and text wrapping for better code display |
| utils.ts | Enhanced freeTrial parameter handling to distinguish between removal and setting |
| product-price-row.tsx | Fixed async state bugs, redesigned UI with better visual feedback |
| product-dialog.tsx | Comprehensive styling updates with improved form layouts and validation |
| page-client.tsx | Redesigned view toggle from switches to modern button group |
| page-client-list-view.tsx | Updated list UI with improved styling, replaced confirm() with ActionDialog |
| page-client-catalogs-view.tsx | Major refactoring with ValidationError class, AI prompt generation, improved product management |
| list-section.tsx | Modernized search UI with better focus states and styling |
| layout.tsx | Redesigned test mode and setup banners with improved information hierarchy |
| components.tsx | Updated popover styling for consistency with new design system |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| useEffect(() => { | ||
| // This effect will run whenever paymentsConfig.products changes | ||
| // The productsKey dependency in useMemo above should handle most cases, | ||
| // but this ensures we catch any edge cases | ||
| }, [productsKey]); |
There was a problem hiding this comment.
This useEffect hook is empty and serves no purpose. The comment on line 1725 explains it's meant to "catch any edge cases", but an empty effect does nothing. The dependency on productsKey would cause the effect to run when products change, but without any code in the effect body, it has no observable effect.
Consider removing this empty effect:
// Remove lines 1724-1728If the intent was to force a re-render, there are better patterns like using the refreshKey state that's already defined on line 1698.
| useEffect(() => { | |
| // This effect will run whenever paymentsConfig.products changes | |
| // The productsKey dependency in useMemo above should handle most cases, | |
| // but this ensures we catch any edge cases | |
| }, [productsKey]); |
| <div className="grid gap-4 sm:grid-cols-2"> | ||
| <div className="flex flex-col gap-1.5"> | ||
| <LabelWithInfo tooltip="The display name shown to customers on checkout pages and invoices"> | ||
| Offer Name |
There was a problem hiding this comment.
[nitpick] The label text "Offer Name" is inconsistent with other parts of the codebase that use "Display Name" (e.g., item-dialog.tsx line 149, product-dialog.tsx line 448). While "Offer Name" might be intentional for product context, this inconsistency could confuse users.
Consider using "Display Name" for consistency, or if "Offer" is specific terminology for products, ensure it's used consistently throughout the product-related UI.
| Offer Name | |
| Display Name |
| value={productId} | ||
| onChange={(e) => { | ||
| const nextValue = e.target.value.toLowerCase(); | ||
| const nextValue = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''); |
There was a problem hiding this comment.
The onChange handler filters the product ID to remove invalid characters using .replace(/[^a-z0-9-]/g, ''), but this differs from the validation pattern used later which is /^[a-z0-9-]+$/. The filter removes underscores if typed, but underscores are allowed in item IDs (as seen in item-dialog.tsx line 40). This creates an inconsistency between products and items.
Consider either:
- Allowing underscores in product IDs to match item IDs:
.replace(/[^a-z0-9_-]/g, '') - Or documenting why products don't allow underscores while items do
| const nextValue = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''); | |
| const nextValue = e.target.value.toLowerCase().replace(/[^a-z0-9_-]/g, ''); |
| const handleCreateItem = (customerType?: 'user' | 'team' | 'custom') => { | ||
| const handleCreateItem = (customerType?: 'user' | 'team' | 'custom', onCreated?: (itemId: string) => void) => { | ||
| setNewItemCustomerType(customerType); | ||
| setOnItemCreatedCallback(() => onCreated); |
There was a problem hiding this comment.
The callback function stored in state is being wrapped in an arrow function on line 1818, which can cause issues. When setOnItemCreatedCallback is called with () => onCreated, it stores a function that returns the callback, not the callback itself.
This should be:
setOnItemCreatedCallback(() => onCreated);Or better yet, store the function directly without wrapping:
const [onItemCreatedCallback, setOnItemCreatedCallback] = useState<((itemId: string) => void) | undefined>(undefined);
// ...
setOnItemCreatedCallback(onCreated);However, looking at line 1836, the callback is invoked correctly as onItemCreatedCallback(itemId), so this might work but is unconventional. The typical pattern would be to store the function reference directly.
| setOnItemCreatedCallback(() => onCreated); | |
| setOnItemCreatedCallback(onCreated); |
| const productIds = Object.entries(paymentsConfig.products) | ||
| .filter(([, product]) => product != null) | ||
| .map(([id]) => id) | ||
| .sort() | ||
| .join(','); |
There was a problem hiding this comment.
The code creates a productsKey memoized value and a productIds variable that serve the same purpose (tracking product changes), but only productsKey is used. The productIds variable on lines 1706-1710 is computed but never used, making it dead code.
Consider removing the unused productIds variable:
// Remove lines 1706-1710| const productIds = Object.entries(paymentsConfig.products) | |
| .filter(([, product]) => product != null) | |
| .map(([id]) => id) | |
| .sort() | |
| .join(','); |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (11)
apps/dashboard/src/components/code-block.tsx (1)
62-87: Consolidate horizontal scrolling to avoid nested scrollbarsThe new
overflow-x-autowrapper (Line 62) plusoverflow: 'auto'incustomStyle(Line 74) means both the parent and the<pre>can create horizontal scrollbars. This can produce awkward nested scrolling for wide code blocks.You can keep the intended behavior (horizontal scroll at the wrapper, vertical scroll inside) by scoping overflow on the highlighter to vertical only:
customStyle={{ background: 'transparent', padding: '1em', border: 0, boxShadow: 'none', margin: 0, fontSize: '0.875rem', maxHeight: props.maxHeight, - overflow: 'auto', + overflowY: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word',This keeps vertical scrolling for tall snippets while the new wrapper cleanly handles horizontal overflow without double scrollbars.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx (2)
131-144: Simplify the onClick handler.The arrow function wrapper is unnecessary since
handleDisableTestModetakes no arguments.Apply this diff:
- onClick={() => handleDisableTestMode()} + onClick={handleDisableTestMode}
184-206: Simplify onClick handlers.Both arrow function wrappers are unnecessary since the handlers take no arguments.
Apply this diff:
- onClick={() => handleEnableTestMode()} + onClick={handleEnableTestMode}- onClick={() => setupPayments()} + onClick={setupPayments}packages/stack-ui/src/components/ui/dropdown-menu.tsx (1)
84-87: Inconsistent indentation in className attribute.The
classNameattribute has unusual indentation that doesn't match the pattern used elsewhere in this file (e.g.,DropdownMenuContenton lines 102-106).<DropdownMenuPrimitive.SubContent ref={ref} - className={cn( - "stack-scope z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border/50 bg-popover p-1.5 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className - )} + className={cn( + "stack-scope z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border/50 bg-popover p-1.5 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} {...props}apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (1)
19-35: Consider extractingLabelWithInfoto a shared location.This component is duplicated in
page-client-catalogs-view.tsx. Consider extracting it tocomponents.tsxor a shared UI utilities file to avoid duplication.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (1)
572-590: Inconsistent deletion pattern between products and items.Products use
typedFromEntries/typedEntriesto rebuild the entire products object (line 446-449), but items use the simpler path notationpayments.items.${itemToDelete.id}: null(line 582). While both work, the inconsistency may cause confusion. Consider using the same approach for both, or document why they differ.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (2)
128-144:LabelWithInfois duplicated across files.This component is identical to the one in
product-price-row.tsx. Extract it tocomponents.tsxalongsideOrSeparator,SectionHeading, andIntervalPopoverto avoid duplication.
524-588: ThegenerateComprehensivePromptfunction is quite large.At ~250 lines, this prompt generation function is substantial. Consider extracting it to a separate utility file (e.g.,
prompt-generator.ts) for better maintainability, especially if the prompt format evolves over time.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (2)
33-45: Header refactor & sticky layout look good; tooltipdisabledprop is redundantThe sticky header, spacing, and typography updates look clean and consistent with the rest of the dashboard. One small simplification: since
SimpleTooltipis only rendered whentitleTooltipis truthy ({titleTooltip && (...)}), passingdisabled={!titleTooltip}is effectively redundant. You can dropdisabledhere, or repurpose it only if you need to explicitly disable the tooltip even when a non-empty string is present.Also applies to: 90-90
47-72: Search input UX is solid; consider minor accessibility improvementsThe search control’s layout, width animation, and transition choices look good and align with the “snappy, hover-exit only” guideline (
transition-all+hover:transition-none). To tighten accessibility:
- Use
type="search"instead oftype="text"for better semantics and mobile keyboards.- Provide an explicit accessible name for the input (placeholder alone is not ideal for screen readers).
- Mark the search icon decorative so it’s ignored by assistive tech.
For example:
- {onSearchChange && ( + {onSearchChange && ( <div className={cn( "relative flex items-center transition-all duration-150 hover:transition-none", isSearchFocused ? "w-[180px]" : "w-[150px]" )}> - <div className="absolute left-3 flex items-center justify-center pointer-events-none z-10"> - <Search className="h-4 w-4 text-foreground/60" /> + <div className="absolute left-3 flex items-center justify-center pointer-events-none z-10"> + <Search + className="h-4 w-4 text-foreground/60" + aria-hidden="true" + focusable="false" + /> </div> - <input - type="text" + <input + type="search" + aria-label={searchPlaceholder} placeholder={searchPlaceholder}(Alternatively, you could wire this up to an external
<label>instead of usingaria-label.)apps/dashboard/src/components/payments/item-dialog.tsx (1)
186-209: Customer type select wiring is fine; consider tightening the value typingThe select wiring (
value,disabled,onValueChange) and the updated styling/helper text look good. If you want to avoid theas typeof customerTypecast, you could narrow the callback parameter instead, for example:onValueChange={(value: 'user' | 'team' | 'custom') => setCustomerType(value)}This keeps type safety localized to the handler without relying on a cast.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx(3 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx(2 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx(2 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx(30 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx(16 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx(3 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx(10 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx(12 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts(4 hunks)apps/dashboard/src/components/code-block.tsx(2 hunks)apps/dashboard/src/components/data-table/payment-product-table.tsx(3 hunks)apps/dashboard/src/components/payments/item-dialog.tsx(5 hunks)packages/stack-ui/src/components/ui/dropdown-menu.tsx(10 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: For blocking alerts and errors, never usetoast, as they are easily missed by the user. Instead, use alerts
Keep hover/click transitions snappy and fast. Don't delay the action with a pre-transition; apply transitions after the action instead
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error). UserunAsynchronouslyorrunAsynchronouslyWithAlertinstead for asynchronous error handling
When creating hover transitions, avoid hover-enter transitions and just use hover-exit transitions (e.g.,transition-colors hover:transition-none)
Use ES6 maps instead of records wherever possible
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/components/data-table/payment-product-table.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxpackages/stack-ui/src/components/ui/dropdown-menu.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
apps/{backend,dashboard}/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
NEVER use Next.js dynamic functions if you can avoid them. Instead, prefer using a client component and prefer
usePathnameinstead ofawait params
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/components/data-table/payment-product-table.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-28T21:21:39.142Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.142Z
Learning: Applies to **/*.{ts,tsx} : Keep hover/click transitions snappy and fast. Don't delay the action with a pre-transition; apply transitions after the action instead
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
🧬 Code graph analysis (8)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (2)
docs/src/components/icons.tsx (1)
ChevronsUpDown(88-91)packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)
apps/dashboard/src/components/data-table/payment-product-table.tsx (1)
packages/stack-shared/src/utils/objects.tsx (2)
typedFromEntries(281-283)typedEntries(263-265)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (1)
packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (2)
packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)docs/src/components/icons.tsx (2)
Search(93-96)Plus(317-320)
packages/stack-ui/src/components/ui/dropdown-menu.tsx (1)
packages/stack-ui/src/index.ts (1)
cn(53-53)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (2)
packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (2)
buildPriceUpdate(65-106)freeTrialLabel(50-55)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (3)
packages/stack-ui/src/components/action-dialog.tsx (1)
ActionDialog(31-135)packages/stack-shared/src/utils/objects.tsx (2)
typedFromEntries(281-283)typedEntries(263-265)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (1)
ListSection(19-97)
apps/dashboard/src/components/payments/item-dialog.tsx (2)
apps/dashboard/src/lib/utils.tsx (1)
cn(7-9)packages/stack-ui/src/components/ui/typography.tsx (1)
Typography(47-47)
🪛 Biome (2.1.2)
apps/dashboard/src/components/data-table/payment-product-table.tsx
[error] 108-108: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
[error] 445-445: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: claude-review
- GitHub Check: Agent
- GitHub Check: CodeQL analysis (javascript-typescript)
- GitHub Check: Vercel Agent Review
- GitHub Check: docker
- GitHub Check: restart-dev-and-test
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: setup-tests
- GitHub Check: all-good
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
🔇 Additional comments (21)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx (1)
42-44: LGTM!The function correctly enables test mode and follows the same pattern as the existing
handleDisableTestMode.packages/stack-ui/src/components/ui/dropdown-menu.tsx (2)
118-120: Well-implemented icon prop for DropdownMenuItem.The icon support is cleanly integrated - the conditional padding logic
(inset || icon) && "pl-9"correctly handles both cases, and the absolutely positioned icon aligns well with the existing patterns used for checkbox/radio indicators.Also applies to: 130-131, 145-149
65-66: Consistent styling updates across dropdown components.The padding, border, and spacing changes are applied uniformly across SubTrigger, Content, CheckboxItem, RadioItem, and Label components, maintaining visual consistency.
Also applies to: 103-103, 164-164, 170-170, 188-188, 193-193, 212-213
apps/dashboard/src/components/data-table/payment-product-table.tsx (1)
53-58: LGTM!Good defensive filtering to handle null products before mapping, preventing rendering issues during deletions.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (1)
63-98: LGTM!The three-way
freeTrialhandling (null to remove, DayInterval to set, undefined to compute) is cleanly implemented and well-documented.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (2)
148-177: LGTM!The native button elements with proper hover/transition styling follow the coding guidelines for snappy transitions ("transition-colors duration-150 hover:transition-none").
180-222: LGTM!The custom interval section is cleanly structured with consistent styling between light and dark modes.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
111-136: LGTM!The inline toggle button group is cleanly implemented with proper transition patterns per coding guidelines.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx (2)
414-424: LGTM!Good input sanitization for Product ID - restricting to lowercase letters, numbers, and hyphens, and clearing the error on change allows users to correct mistakes without lingering errors.
293-316: LGTM!Template cards have clean hover states with consistent transition patterns following the coding guidelines.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (2)
105-120: LGTM!The type change from
Partial<ReturnType<...>>toPartial<Parameters<...>[0]>is correct since we're passing input parameters tobuildPriceUpdate, not its output.
246-249: LGTM!Passing
freeTrial: nullto explicitly remove the free trial integrates correctly with the updatedbuildPriceUpdatefunction's three-way handling.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (2)
612-618: LGTM!Good defensive filtering to skip null products when creating refs, ensuring consistency after deletions.
632-640: LGTM!Proper null/undefined filtering during product grouping prevents rendering issues.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (3)
46-52: LGTM!Good pattern - using a custom
ValidationErrorclass to distinguish validation failures (which should keep edit mode open) from other errors (which should propagate).
1863-1900: LGTM!The deletion logic properly rebuilds the products object and handles the edge case of deleting the last product in a catalog by also removing the empty catalog.
1566-1576: LGTM!Using
ValidationErrorto signal validation failures without closing edit mode is clean - thethrowprevents the draft from being removed while the toast provides user feedback.apps/dashboard/src/components/payments/item-dialog.tsx (4)
4-5: Imports align with component usageAll newly added imports (
Button,Typography,Package, etc.) are used in this file and match the updated UI structure; nothing appears unused or redundant.
40-41: Item ID sanitization and validation are consistentThe input sanitization (
toLowerCase().replace(/[^a-z0-9_-]/g, '')), the validation regex (^[a-z0-9_-]+$), and the updated error/description copy are aligned with each other and prevent inconsistent states. The conditional error styling on the input also looks correct.Also applies to: 115-133, 136-143
83-88: Dialog layout and header redesign look structurally soundThe new
DialogContentsizing/styling and the richer header (icon, title, description) maintain proper semantics withDialogTitle/DialogDescriptionwhile matching the updated visual system. No structural or accessibility regressions stand out.Also applies to: 89-103, 105-105
148-150: Display name field error handling and helper text are clearThe display name label, input styling, conditional error text, and muted helper text follow the same pattern as the item ID field and give good guidance without extra complexity. Error clearing on change is also handled correctly.
Also applies to: 165-171, 173-181
| {onAddClick && ( | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| className={cn( | ||
| "h-9 w-9 p-0 rounded-xl", | ||
| "text-muted-foreground hover:text-foreground", | ||
| "hover:bg-foreground/[0.06] border border-transparent hover:border-border/40 dark:hover:border-foreground/[0.08]", | ||
| "transition-all duration-150 hover:transition-none" | ||
| )} | ||
| onClick={onAddClick} | ||
| > | ||
| <Plus className="h-4 w-4" /> | ||
| </Button> | ||
| )} | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "list-section.tsx" | grep -E "products|payments"Repository: stack-auth/stack-auth
Length of output: 164
🏁 Script executed:
cat -n apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/list-section.tsx | head -150Repository: stack-auth/stack-auth
Length of output: 4406
🏁 Script executed:
find . -type f -name "*.tsx" -o -name "*.ts" | xargs grep -l "export.*Button" | grep -E "stackframe|stack-ui" | head -5Repository: stack-auth/stack-auth
Length of output: 164
🏁 Script executed:
rg "ListSection" --type tsx --type ts -B 2 -A 2 | head -100Repository: stack-auth/stack-auth
Length of output: 92
🏁 Script executed:
rg "aria-label" --type tsx --type ts -B 1 -A 1 | grep -E "Button|IconButton|add" | head -30Repository: stack-auth/stack-auth
Length of output: 92
🏁 Script executed:
cat packages/stack-ui/src/components/ui/button.tsxRepository: stack-auth/stack-auth
Length of output: 3521
🏁 Script executed:
cat packages/stack-ui/src/components/copy-button.tsx | head -80Repository: stack-auth/stack-auth
Length of output: 1056
🏁 Script executed:
rg "ListSection" -A 3 -B 1 | head -100Repository: stack-auth/stack-auth
Length of output: 11297
🏁 Script executed:
grep -A 5 "onAddClick" apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/page-client-list-view.tsxRepository: stack-auth/stack-auth
Length of output: 436
🏁 Script executed:
grep -A 8 "ListSection" apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/product-dialog.tsx | grep -E "ListSection|title|onAddClick" | head -20Repository: stack-auth/stack-auth
Length of output: 403
Icon-only "add" button should expose an accessible label
The visual styling and transition behavior on the add button look good and match the rest of the header. However, the button currently renders only a <Plus> icon with no text or aria-label, so assistive technologies will announce it as an unnamed "button", which is problematic for keyboard and screen-reader users.
Add an addButtonAriaLabel prop to ListSectionProps and wire it to the button:
export type ListSectionProps = {
title: React.ReactNode,
titleTooltip?: string,
onAddClick?: () => void,
addButtonAriaLabel?: string, // e.g. "Add product", "Add price", "Add item"
children: ReactNode,
hasTitleBorder?: boolean,
searchValue?: string,
onSearchChange?: (value: string) => void,
searchPlaceholder?: string,
};Then apply it on the button:
{onAddClick && (
<Button
variant="ghost"
size="sm"
className={cn(
"h-9 w-9 p-0 rounded-xl",
"text-muted-foreground hover:text-foreground",
"hover:bg-foreground/[0.06] border border-transparent hover:border-border/40 dark:hover:border-foreground/[0.08]",
"transition-all duration-150 hover:transition-none"
)}
onClick={onAddClick}
+ aria-label={addButtonAriaLabel}
>
<Plus className="h-4 w-4" />
</Button>
)}Each usage site can then provide a context-specific label ("Add product", "Add price", "Add item") while keeping the visual design clean.
🤖 Prompt for AI Agents
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx
around lines 73 to 88: the icon-only add button lacks an accessible label;
update the ListSectionProps type to include addButtonAriaLabel?: string and pass
that prop to the Button as its aria-label (e.g.,
aria-label={addButtonAriaLabel}) when rendering the Plus-only button so callers
can supply context-specific labels like "Add product" or "Add price"; ensure
prop is optional and the Button receives the aria-label only when provided (or
provide a sensible default if desired).
.../app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
Outdated
Show resolved
Hide resolved
| onClick: async () => { | ||
| if (!productToDelete) return; | ||
| const config = project.useConfig(); | ||
| const updatedProducts = typedFromEntries( | ||
| typedEntries(config.payments.products) | ||
| .filter(([productId]) => productId !== productToDelete.id) | ||
| ); | ||
| await project.updateConfig({ "payments.products": updatedProducts }); | ||
| toast({ title: "Product deleted" }); | ||
| setProductToDelete(null); | ||
| } | ||
| }} |
There was a problem hiding this comment.
Hook called inside event handler - violates Rules of Hooks.
project.useConfig() at line 445 is called inside an async onClick handler. This violates React's Rules of Hooks and will cause errors.
The project already has access to config at the component level. Move the config retrieval outside the callback:
function ProductsList({
...
}: ProductsListProps) {
const stackAdminApp = useAdminApp();
const project = stackAdminApp.useProject();
+ const config = project.useConfig();
const [searchQuery, setSearchQuery] = useState("");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [productToDelete, setProductToDelete] = useState<{ id: string, displayName: string } | null>(null);
...
okButton={{
label: "Delete",
onClick: async () => {
if (!productToDelete) return;
- const config = project.useConfig();
const updatedProducts = typedFromEntries(
typedEntries(config.payments.products)
.filter(([productId]) => productId !== productToDelete.id)
);
...
}
}}Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 Biome (2.1.2)
[error] 445-445: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
around lines 443 to 454, move the call to project.useConfig() out of the async
onClick handler to the component-level (above render or inside the component
body) so hooks run unconditionally; then have the onClick handler reference that
component-level config (or a derived state/const) and compute updatedProducts
and call project.updateConfig(...) from the handler; ensure you still clear
productToDelete and show the toast inside the handler and remove any hook calls
from within event callbacks.
| <DialogFooter className="pt-4 border-t border-border/30 dark:border-foreground/[0.06] gap-2"> | ||
| <Button | ||
| variant="outline" | ||
| onClick={handleClose} | ||
| className={cn( | ||
| "rounded-xl px-5", | ||
| "border-border/50 dark:border-foreground/[0.1]", | ||
| "hover:bg-foreground/[0.03]", | ||
| "transition-all duration-150 hover:transition-none" | ||
| )} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button onClick={validateAndSave}> | ||
| <Button | ||
| onClick={validateAndSave} | ||
| className={cn( | ||
| "rounded-xl px-5", | ||
| "bg-foreground text-background", | ||
| "hover:bg-foreground/90", | ||
| "transition-all duration-150 hover:transition-none" | ||
| )} | ||
| > |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the file around lines 213-234 to see the current state
cat -n apps/dashboard/src/components/payments/item-dialog.tsx | sed -n '200,240p'Repository: stack-auth/stack-auth
Length of output: 1857
🏁 Script executed:
# Search for the validateAndSave function in the file
rg "validateAndSave" apps/dashboard/src/components/payments/item-dialog.tsx -B 5 -A 10Repository: stack-auth/stack-auth
Length of output: 1357
🏁 Script executed:
# Search for runAsynchronouslyWithAlert usage to understand the pattern in this codebase
rg "runAsynchronouslyWithAlert" apps/dashboard/src/components/ -l | head -5Repository: stack-auth/stack-auth
Length of output: 298
🏁 Script executed:
# Check what async handling utilities are available
fd -e ts -e tsx apps/dashboard/src/ | xargs rg "export.*runAsynchronously" | head -10Repository: stack-auth/stack-auth
Length of output: 817
🏁 Script executed:
# Check how runAsynchronouslyWithAlert is imported in existing dashboard files
rg "runAsynchronouslyWithAlert" apps/dashboard/src/components/form-dialog.tsx -B 10 | grep -E "(import|from)" | head -3Repository: stack-auth/stack-auth
Length of output: 210
🏁 Script executed:
# Get more context on usage pattern in form-dialog.tsx
rg "onClick.*runAsynchronouslyWithAlert" apps/dashboard/src/components/form-dialog.tsx -B 2 -A 2Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
# Check actual usage in form-dialog.tsx with more context
rg "onClick" apps/dashboard/src/components/form-dialog.tsx -B 2 -A 2Repository: stack-auth/stack-auth
Length of output: 390
🏁 Script executed:
# Also check if item-dialog.tsx already imports the utility
rg "runAsynchronously" apps/dashboard/src/components/payments/item-dialog.tsxRepository: stack-auth/stack-auth
Length of output: 47
Wrap async validateAndSave with runAsynchronouslyWithAlert to handle promise rejections
validateAndSave is an async function called directly from onClick at line 227, which will create an unhandled promise rejection if an error occurs. Per coding guidelines, wrap it with runAsynchronouslyWithAlert to surface errors via alerts:
onClick={() => runAsynchronouslyWithAlert(validateAndSave)}Add the import from @stackframe/stack-shared/dist/utils/promises:
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";🤖 Prompt for AI Agents
In apps/dashboard/src/components/payments/item-dialog.tsx around lines 213 to
234, the async validateAndSave is invoked directly in the Button onClick which
can cause unhandled promise rejections; import runAsynchronouslyWithAlert from
@stackframe/stack-shared/dist/utils/promises and change the onClick to call
runAsynchronouslyWithAlert(validateAndSave) (e.g. onClick={() =>
runAsynchronouslyWithAlert(validateAndSave)}) so errors are caught and surfaced
via alerts.
dcfab53 to
3293d98
Compare
Preview Screenshots⏳ Preview screenshots are being captured... Workspace and dev browser links will appear here once the preview environment is ready. Generated by cmux preview system |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (10)
apps/dashboard/src/components/data-table/payment-product-table.tsx (1)
107-115: Hook called inside event handler - violates Rules of Hooks.
project.useConfig()at line 108 is called inside an asynconClickhandler. Hooks must be called unconditionally at the top level of a component, not inside callbacks.Move the hook call to component level:
function ActionsCell({ product }: { product: PaymentProduct }) { const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const stackAdminApp = useAdminApp(); const project = stackAdminApp.useProject(); + const config = project.useConfig(); return ( <> ... <ActionDialog ... okButton={{ label: "Delete", onClick: async () => { - const config = project.useConfig(); const updatedProducts = typedFromEntries( typedEntries(config.payments.products) .filter(([productId]) => productId !== product.id) ); await project.updateConfig({ "payments.products": updatedProducts }); toast({ title: "Product deleted" }); }, }} /> </> ); }apps/dashboard/src/components/payments/item-dialog.tsx (1)
226-236: Wrap asyncvalidateAndSavewithrunAsynchronouslyWithAlert.This issue was flagged in a previous review. The async
validateAndSaveis still called directly fromonClick, creating unhandled promise rejections on failure.+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";<Button - onClick={validateAndSave} + onClick={() => runAsynchronouslyWithAlert(validateAndSave)} ... >apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (1)
73-87: Addaria-labelto the icon-only button for accessibility.This was flagged in a previous review. The button renders only a
<Plus>icon with no accessible label, making it unidentifiable for screen reader users.Add an
addButtonAriaLabelprop toListSectionPropsand apply it:export type ListSectionProps = { title: React.ReactNode, titleTooltip?: string, onAddClick?: () => void, + addButtonAriaLabel?: string, // ... };<Button variant="ghost" size="sm" className={...} onClick={onAddClick} + aria-label={addButtonAriaLabel} > <Plus className="h-4 w-4" /> </Button>apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (5)
1818-1818: Simplify callback storage.The arrow function wrapper
() => onCreatedis the setState updater form, which works but is unconventional for this use case. SinceonCreatedis never undefined in this context, store it directly.- setOnItemCreatedCallback(() => onCreated); + setOnItemCreatedCallback(onCreated);This is clearer and more conventional.
1198-1198: Missing error handling wrapper for async onClick.While
handleSaveEdithas internal error handling for ValidationError, any other errors would be re-thrown and need to be caught byrunAsynchronouslyWithAlert.Apply this diff:
- onClick={async () => { await handleSaveEdit(); }} + onClick={() => runAsynchronouslyWithAlert(handleSaveEdit)}Don't forget to import
runAsynchronouslyWithAlertfrom@stackframe/stack-shared/dist/utils/promises.Based on coding guidelines and learnings.
1341-1343: Missing error handling wrapper for async onClick.The async onClick handler needs
runAsynchronouslyWithAlertto properly handle errors.Apply this diff:
okButton={{ label: "Delete", - onClick: async () => { + onClick: () => runAsynchronouslyWithAlert(async () => { await onDelete(id); setIsEditing(false); - } + }) }}Based on coding guidelines and learnings.
1724-1728: Remove empty useEffect.This effect has an empty body and serves no purpose. The comment suggests it's a safeguard, but re-rendering is already handled by the
productsKeydependency in theuseMemoabove.- // Watch for changes in products and force re-render if needed - useEffect(() => { - // This effect will run whenever paymentsConfig.products changes - // The productsKey dependency in useMemo above should handle most cases, - // but this ensures we catch any edge cases - }, [productsKey]);
1706-1710: Remove unused productIds variable.The
productIdsvariable is computed but never used. OnlyproductsKey(lines 1714-1721) is referenced in the code.- const productIds = Object.entries(paymentsConfig.products) - .filter(([, product]) => product != null) - .map(([id]) => id) - .sort() - .join(','); -apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (2)
443-453: CRITICAL: Hook violation and missing error handling in async onClick.This code has two critical issues:
Hook called inside event handler (line 445):
project.useConfig()is called inside the asynconClickhandler, violating React's Rules of Hooks. Hooks must be called unconditionally at the top level.Missing error handling: The async
onClickhandler needsrunAsynchronouslyWithAlertto properly handle errors and show alerts to users.Apply this diff to fix both issues:
function ProductsList({ ... }: ProductsListProps) { const stackAdminApp = useAdminApp(); const project = stackAdminApp.useProject(); + const config = project.useConfig(); const [searchQuery, setSearchQuery] = useState(""); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [productToDelete, setProductToDelete] = useState<{ id: string, displayName: string } | null>(null); ... okButton={{ label: "Delete", - onClick: async () => { + onClick: () => runAsynchronouslyWithAlert(async () => { if (!productToDelete) return; - const config = project.useConfig(); const updatedProducts = typedFromEntries( typedEntries(config.payments.products) .filter(([productId]) => productId !== productToDelete.id) ); await project.updateConfig({ "payments.products": updatedProducts }); toast({ title: "Product deleted" }); setProductToDelete(null); - } + }) }}Based on coding guidelines and learnings.
580-585: Missing error handling in async onClick.The async
onClickhandler needsrunAsynchronouslyWithAlertto properly handle errors and show alerts to users.Apply this diff:
okButton={{ label: "Delete", - onClick: async () => { + onClick: () => runAsynchronouslyWithAlert(async () => { if (!itemToDelete) return; await project.updateConfig({ [`payments.items.${itemToDelete.id}`]: null }); toast({ title: "Item deleted" }); setItemToDelete(null); - } + }) }}Based on coding guidelines and learnings.
🧹 Nitpick comments (4)
apps/dashboard/src/components/code-block.tsx (1)
62-87: Avoid nested scrollbars + clarify “wrap vs horizontal scroll” behavior.Right now the outer
overflow-x-auto(Line 62) + inneroverflow: 'auto'(Line 74) can produce nested/duplicated scrollbars, andwhiteSpace: 'pre-wrap'+wordBreak: 'break-word'(Lines 75-76) +wrapLongLines(Line 83) may make horizontal scrolling effectively unreachable (everything wraps). Consider delegating horizontal scrolling to the wrapper and keeping vertical scrolling on the highlighter:customStyle={{ background: 'transparent', padding: '1em', border: 0, boxShadow: 'none', margin: 0, fontSize: '0.875rem', maxHeight: props.maxHeight, - overflow: 'auto', + overflowY: 'auto', + overflowX: 'visible', whiteSpace: 'pre-wrap', wordBreak: 'break-word', ...(props.compact && { padding: '0.75em', fontSize: '0.75rem', }), }}Please double-check
react-syntax-highlighter@15.6.1semantics: whetherwrapLongLinesalready applies thewhiteSpace/wordBreakstyles (making Lines 75-76 redundant), and whether the desired UX is “wrap by default” or “preserve horizontal scroll for code fidelity.”apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (2)
140-145: Optional: Redundant pointer-events-none class.The
pointer-events-noneclass applied whenreadOnlyis true is redundant since thedisabledprop on the PopoverTrigger (which forwards to the button viaasChild) already prevents user interaction. While harmless, you can simplify the conditional class application.- <button type="button" className={cn(triggerClasses, readOnly && "cursor-default pointer-events-none")}> + <button type="button" className={cn(triggerClasses, readOnly && "cursor-default")}>
184-199: Strengthen input validation to handle edge cases.The current validation at line 195 doesn't explicitly check for
NaN, which can occur when the input is cleared or contains non-numeric characters. While the> 0check implicitly preventsNaNfrom being processed (sinceNaN > 0is false), the code would be more robust and maintainable with an explicit check.<Input type="number" min={1} className={cn( "w-20 h-9 text-sm", "rounded-lg border border-border/60 dark:border-foreground/[0.1]", "bg-background dark:bg-[hsl(240,10%,10%)]" )} value={effectiveSelection === 'custom' ? count : 1} onChange={(e) => { const val = parseInt(e.target.value, 10); - if (val > 0) { + if (!isNaN(val) && val > 0) { applyCustom(val, effectiveUnit); } }} />apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (1)
128-144: Extract duplicated LabelWithInfo component to shared location.The LabelWithInfo component is duplicated between this file and
product-price-row.tsx. Extract it to a shared location to follow DRY principles.Consider moving it to a shared components directory:
// apps/dashboard/src/components/label-with-info.tsx import { Label, SimpleTooltip } from "@stackframe/stack-ui"; import { Info } from "lucide-react"; export function LabelWithInfo({ children, tooltip }: { children: React.ReactNode, tooltip?: string }) { return ( <div className="flex items-center gap-1.5"> <Label className="text-[11px] font-medium uppercase tracking-[0.24em] text-muted-foreground"> {children} </Label> {tooltip && ( <SimpleTooltip tooltip={tooltip}> <Info className="h-3 w-3 text-muted-foreground/60 cursor-help" /> </SimpleTooltip> )} </div> ); }Then import from both files.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx(3 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx(2 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx(2 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx(30 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx(16 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx(3 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx(10 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx(12 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts(4 hunks)apps/dashboard/src/components/code-block.tsx(2 hunks)apps/dashboard/src/components/data-table/payment-product-table.tsx(3 hunks)apps/dashboard/src/components/payments/item-dialog.tsx(5 hunks)packages/stack-ui/src/components/ui/dropdown-menu.tsx(10 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx
- packages/stack-ui/src/components/ui/dropdown-menu.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Always add new E2E tests when changing the API or SDK interface
For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Use ES6 maps instead of records wherever you can
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,css}: Keep hover/click transitions snappy and fast; avoid fade-in delays on hover. Apply transitions after action completion instead, like smooth fade-out when hover ends
Use hover-exit transitions instead of hover-enter transitions; for example, use 'transition-colors hover:transition-none' instead of fade-in on hover
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
apps/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
{.env*,**/*.{ts,tsx,js}}
📄 CodeRabbit inference engine (AGENTS.md)
Prefix environment variables with STACK_ (or NEXT_PUBLIC_STACK_ if public) so changes are picked up by Turborepo and improves readability
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
apps/dashboard/**/*
📄 CodeRabbit inference engine (AGENTS.md)
When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
apps/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Files:
apps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
🧠 Learnings (5)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/payments/item-dialog.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : Always add new E2E tests when changing the API or SDK interface
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/**/*.tsx : Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Applied to files:
apps/dashboard/src/components/data-table/payment-product-table.tsx
🧬 Code graph analysis (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (2)
packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)docs/src/components/icons.tsx (2)
Search(93-96)Plus(317-320)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (1)
packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (3)
apps/e2e/tests/js/convex.test.ts (1)
open(31-33)docs/src/components/icons.tsx (1)
ChevronsUpDown(88-91)packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (3)
packages/stack-ui/src/components/action-dialog.tsx (1)
ActionDialog(31-135)packages/stack-shared/src/utils/objects.tsx (2)
typedFromEntries(281-283)typedEntries(263-265)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (1)
ListSection(19-97)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (6)
Product(8-8)getPricesObject(111-121)intervalLabel(26-34)Price(9-9)PricesObject(10-10)shortIntervalLabel(39-45)packages/stack-shared/src/utils/numbers.tsx (1)
prettyPrintWithMagnitudes(9-26)packages/stack-shared/src/utils/objects.tsx (2)
typedEntries(263-265)typedFromEntries(281-283)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (2)
packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (2)
buildPriceUpdate(65-106)freeTrialLabel(50-55)
apps/dashboard/src/components/data-table/payment-product-table.tsx (1)
packages/stack-shared/src/utils/objects.tsx (2)
typedFromEntries(281-283)typedEntries(263-265)
🪛 Biome (2.1.2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
[error] 445-445: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
apps/dashboard/src/components/data-table/payment-product-table.tsx
[error] 108-108: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
- GitHub Check: Vercel Agent Review
- GitHub Check: setup-tests
- GitHub Check: restart-dev-and-test
- GitHub Check: all-good
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
- GitHub Check: docker
- GitHub Check: setup-tests-with-custom-base-port
- GitHub Check: build (22.x)
- GitHub Check: lint_and_build (latest)
🔇 Additional comments (10)
apps/dashboard/src/components/data-table/payment-product-table.tsx (1)
53-58: LGTM!Good defensive approach to filter out null products before mapping. This correctly handles the case where products may be nullified during deletion.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (1)
25-33: LGTM! Clean styling refactor.The replacement of the Separator component with native div elements maintains the same visual appearance while reducing dependencies. The styling is consistent and appropriate.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (1)
85-98: LGTM! Clear three-way logic for freeTrial handling.The explicit null/DayInterval/undefined branching cleanly separates removal, direct assignment, and computed defaults. The JSDoc at line 63 documents this well.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx (1)
88-147: Good use of hover-exit transition pattern.The
transition-colors duration-150 hover:transition-nonepattern at line 140 correctly follows the coding guidelines for snappy hover interactions with smooth fade-out.apps/dashboard/src/components/payments/item-dialog.tsx (1)
40-41: LGTM! Validation and sanitization regexes are aligned.Both the validation (
/^[a-z0-9_-]+$/) and sanitization (/[^a-z0-9_-]/g) patterns now consistently allow underscores, and the error message accurately reflects this.Also applies to: 115-116
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
111-136: LGTM! Clean view toggle implementation.The inline button group uses accessible text labels and follows the hover-exit transition pattern correctly. Synchronous
setViewhandlers don't require async error handling.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (1)
56-70: LGTM! Well-structured search input with proper styling.The search input follows the hover-exit transition pattern and maintains consistent styling with other inputs in this PR.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (2)
19-35: LGTM! Well-structured helper component.The LabelWithInfo component provides a clean abstraction for labels with optional info tooltips. The implementation correctly uses SimpleTooltip and follows React best practices.
183-188: Direct parameter passing in onChange is correct.The code correctly passes the new value directly to
savePriceUpdatesincesetStateis async. The guard in the useEffect at line 89 (if (isEditing) return;) prevents external updates from interfering during editing.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (1)
1863-1900: LGTM! Proper deletion handling with catalog cleanup.The function correctly:
- Uses
typedFromEntriesto rebuild the products object without the deleted product- Cleans up empty catalogs when deleting the last product
- Forces a re-render with
refreshKeyto ensure UI consistencyThis is the correct pattern for deletions.
| <button | ||
| className={cn( | ||
| "flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium", | ||
| "transition-colors duration-150 hover:transition-none", | ||
| effectiveSelection === 'one-time' | ||
| ? "bg-foreground/[0.08] text-foreground" | ||
| : "hover:bg-foreground/[0.04] text-foreground" | ||
| )} | ||
| onClick={selectOneTime} | ||
| > | ||
| {noneLabel} | ||
| </Button> | ||
| </button> | ||
|
|
||
| {/* Fixed interval options */} | ||
| {normalizedUnits.map((unitOption) => ( | ||
| <Button | ||
| <button | ||
| key={unitOption} | ||
| variant={effectiveSelection === unitOption ? 'secondary' : 'ghost'} | ||
| size="sm" | ||
| className="justify-start" | ||
| className={cn( | ||
| "flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium", | ||
| "transition-colors duration-150 hover:transition-none", | ||
| effectiveSelection === unitOption | ||
| ? "bg-foreground/[0.08] text-foreground" | ||
| : "hover:bg-foreground/[0.04] text-foreground" | ||
| )} | ||
| onClick={() => selectFixed(unitOption)} | ||
| > | ||
| {buttonLabels[unitOption]} | ||
| </Button> | ||
| </button> | ||
| ))} |
There was a problem hiding this comment.
Add type="button" to prevent unintended form submissions.
The one-time and fixed interval buttons are missing the type="button" attribute. When used inside a form context, these buttons will default to type="submit" and could trigger form submission instead of just selecting an interval option.
Apply this diff to fix the issue:
<button
+ type="button"
className={cn(
"flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium",
"transition-colors duration-150 hover:transition-none",
effectiveSelection === 'one-time'
? "bg-foreground/[0.08] text-foreground"
: "hover:bg-foreground/[0.04] text-foreground"
)}
onClick={selectOneTime}
>
{noneLabel}
</button>
{/* Fixed interval options */}
{normalizedUnits.map((unitOption) => (
<button
key={unitOption}
+ type="button"
className={cn(
"flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium",
"transition-colors duration-150 hover:transition-none",
effectiveSelection === unitOption
? "bg-foreground/[0.08] text-foreground"
: "hover:bg-foreground/[0.04] text-foreground"
)}
onClick={() => selectFixed(unitOption)}
>
{buttonLabels[unitOption]}
</button>
))}📝 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.
| <button | |
| className={cn( | |
| "flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium", | |
| "transition-colors duration-150 hover:transition-none", | |
| effectiveSelection === 'one-time' | |
| ? "bg-foreground/[0.08] text-foreground" | |
| : "hover:bg-foreground/[0.04] text-foreground" | |
| )} | |
| onClick={selectOneTime} | |
| > | |
| {noneLabel} | |
| </Button> | |
| </button> | |
| {/* Fixed interval options */} | |
| {normalizedUnits.map((unitOption) => ( | |
| <Button | |
| <button | |
| key={unitOption} | |
| variant={effectiveSelection === unitOption ? 'secondary' : 'ghost'} | |
| size="sm" | |
| className="justify-start" | |
| className={cn( | |
| "flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium", | |
| "transition-colors duration-150 hover:transition-none", | |
| effectiveSelection === unitOption | |
| ? "bg-foreground/[0.08] text-foreground" | |
| : "hover:bg-foreground/[0.04] text-foreground" | |
| )} | |
| onClick={() => selectFixed(unitOption)} | |
| > | |
| {buttonLabels[unitOption]} | |
| </Button> | |
| </button> | |
| ))} | |
| <button | |
| type="button" | |
| className={cn( | |
| "flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium", | |
| "transition-colors duration-150 hover:transition-none", | |
| effectiveSelection === 'one-time' | |
| ? "bg-foreground/[0.08] text-foreground" | |
| : "hover:bg-foreground/[0.04] text-foreground" | |
| )} | |
| onClick={selectOneTime} | |
| > | |
| {noneLabel} | |
| </button> | |
| {/* Fixed interval options */} | |
| {normalizedUnits.map((unitOption) => ( | |
| <button | |
| type="button" | |
| key={unitOption} | |
| className={cn( | |
| "flex items-center w-full px-3 py-2 rounded-lg text-left text-sm font-medium", | |
| "transition-colors duration-150 hover:transition-none", | |
| effectiveSelection === unitOption | |
| ? "bg-foreground/[0.08] text-foreground" | |
| : "hover:bg-foreground/[0.04] text-foreground" | |
| )} | |
| onClick={() => selectFixed(unitOption)} | |
| > | |
| {buttonLabels[unitOption]} | |
| </button> | |
| ))} |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx
around lines 149 to 177, the one-time button and each fixed-interval button lack
an explicit type attribute and will default to type="submit" inside a form;
update both the standalone one-time <button> and the mapped buttons to include
type="button" to prevent accidental form submissions, ensuring each button
element has type="button" while leaving existing props (className, onClick,
children) unchanged.
| <Button | ||
| size="sm" | ||
| variant="outline" | ||
| onClick={() => handleDisableTestMode()} | ||
| className={cn( | ||
| "shrink-0 text-xs font-medium", | ||
| "border-blue-500/30 dark:border-blue-400/30", | ||
| "text-blue-700 dark:text-blue-300", | ||
| "hover:bg-blue-500/10 dark:hover:bg-blue-400/10", | ||
| "transition-colors duration-150 hover:transition-none" | ||
| )} | ||
| > | ||
| Disable test mode | ||
| </Button> |
There was a problem hiding this comment.
Wrap async handlers with runAsynchronouslyWithAlert to handle rejections.
The handleDisableTestMode function is async but called directly from onClick, creating an unhandled promise rejection if it fails. The same issue exists for handleEnableTestMode (line 187) and setupPayments (line 201).
Per coding guidelines, wrap these with runAsynchronouslyWithAlert:
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; <Button
size="sm"
variant="outline"
- onClick={() => handleDisableTestMode()}
+ onClick={() => runAsynchronouslyWithAlert(handleDisableTestMode)}
...
>Apply the same pattern to the other async handlers at lines 187 and 201.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
around lines 131–144 (and also update the similar cases at line 187 and line
201), the async onclick handlers are invoked directly which can cause unhandled
promise rejections; wrap the calls in runAsynchronouslyWithAlert so the async
function is executed through that helper (e.g., replace onClick={() =>
handleDisableTestMode()} with onClick={() => runAsynchronouslyWithAlert(() =>
handleDisableTestMode())}) and apply the same wrapping for handleEnableTestMode
at line 187 and setupPayments at line 201 to ensure rejections are caught and
surfaced via alerts.
| import type { DayInterval } from "@stackframe/stack-shared/dist/utils/dates"; | ||
| import { prettyPrintWithMagnitudes } from "@stackframe/stack-shared/dist/utils/numbers"; | ||
| import { typedEntries } from "@stackframe/stack-shared/dist/utils/objects"; | ||
| import { typedEntries, typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add missing import for error handling utility.
The fixes for error handling require importing runAsynchronouslyWithAlert. Add it to your imports:
import { typedEntries, typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects";
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
around line 10, the code imports typedEntries and typedFromEntries but is
missing the runAsynchronouslyWithAlert utility needed for the new error
handling; add a new named import for runAsynchronouslyWithAlert from
"@stackframe/stack-shared/dist/utils/errors" (or the appropriate errors util
module used in the repo) and use that import in the error handling fixes.
.../src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Additional Suggestion:
The items list crashes when trying to sort after an item is deleted, because the deletion sets items to null but the sort function tries to access properties on null values.
View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
index f6a96b47..0064a6cc 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
@@ -492,15 +492,17 @@ function ItemsList({
// Sort items by customer type, then by ID
const sortedItems = useMemo(() => {
const customerTypePriority = { user: 1, team: 2, custom: 3 };
- return Object.entries(items).sort(([aId, aItem]: [string, any], [bId, bItem]: [string, any]) => {
- const priorityA = customerTypePriority[aItem.customerType as keyof typeof customerTypePriority] || 4;
- const priorityB = customerTypePriority[bItem.customerType as keyof typeof customerTypePriority] || 4;
- if (priorityA !== priorityB) {
- return priorityA - priorityB;
- }
- // If same customer type, sort by ID
- return stringCompare(aId, bId);
- });
+ return Object.entries(items)
+ .filter(([, item]) => item != null)
+ .sort(([aId, aItem]: [string, any], [bId, bItem]: [string, any]) => {
+ const priorityA = customerTypePriority[aItem.customerType as keyof typeof customerTypePriority] || 4;
+ const priorityB = customerTypePriority[bItem.customerType as keyof typeof customerTypePriority] || 4;
+ if (priorityA !== priorityB) {
+ return priorityA - priorityB;
+ }
+ // If same customer type, sort by ID
+ return stringCompare(aId, bId);
+ });
}, [items]);
// Filter items based on search query
@@ -619,8 +621,9 @@ export default function PageClient() {
const itemRefs = useMemo(() => {
const refs = Object.fromEntries(
- Object.keys(paymentsConfig.items)
- .map(id => [id, React.createRef<HTMLDivElement>()])
+ Object.entries(paymentsConfig.items)
+ .filter(([, item]) => item != null)
+ .map(([id]) => [id, React.createRef<HTMLDivElement>()])
);
return refs;
}, [paymentsConfig.items]);
Analysis
TypeError when sorting items after deletion
What fails: ItemsList component in page-client-list-view.tsx crashes when rendering after an item is deleted, because the sortedItems useMemo tries to access customerType property on null values.
How to reproduce:
- Navigate to the payments/products page
- Create an item (has a customerType property)
- Delete the item (sets it to null in the config via
updateConfig({ ['payments.items.{id}']: null })) - Observe the component re-render
Result: TypeError: Cannot read properties of null (reading 'customerType') at line 495 where the sort function accesses aItem.customerType on a null item
Expected: Items list should gracefully handle and skip null items during sorting, consistent with how the products sorting code handles deleted products
Root cause: The sortedItems useMemo at lines 492-504 did not filter out null items before sorting, unlike the similar productRefs code at line 614 which correctly filters with .filter(([, product]) => product != null)
Fix applied:
- Added
.filter(([, item]) => item != null)afterObject.entries(items)in the sortedItems useMemo (line 495) - Applied the same null filter to
itemRefsuseMemo (line 623) for consistency with productRefs pattern
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (12)
apps/dashboard/src/components/editable-grid.tsx (2)
136-144: Missing error handling inhandleChangeswallows failures.The try/finally block clears
isUpdatingbut silently swallows any error thrown byonUpdate. Per coding guidelines and past review feedback, userunAsynchronouslyWithAlertor re-throw errors after cleanup so failures are surfaced to the user.Based on learnings, use
runAsynchronouslyWithAlertfromstackframe/stack-shared/dist/utils/promisesfor async handlers.
195-203: Missing error handling inhandleChangeswallows failures.Same issue as
EditableBooleanField: the try/finally block clearsisUpdatingbut silently swallows errors fromonUpdate. UserunAsynchronouslyWithAlertor re-throw after cleanup.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (10)
97-114: Missing null check for catalog reference.Line 99 accesses
config.payments.catalogs[catalogId]without verifying the catalog exists. If the catalog was deleted but the reference remains, accessing.displayNamewill throw a TypeError.Add a null check or fallback:
- const catalogName = catalogId ? config.payments.catalogs[catalogId].displayName || catalogId : null; + const catalogName = catalogId ? config.payments.catalogs[catalogId]?.displayName || catalogId : null;
129-138: Missing null check for parent product reference.Line 136 accesses
config.payments.products[parentId].displayNamewithout verifying the parent product exists. If a parent product was deleted but remains inisAddOnTo, this will throw a TypeError.Add optional chaining:
- displayName: config.payments.products[parentId].displayName || parentId, + displayName: config.payments.products[parentId]?.displayName || parentId,
277-283: Missing null check for parent product reference.Line 281 accesses
config.payments.products[parentId].displayNamewithout verifying the parent product exists, identical to the issue in ProductHeader.Add optional chaining:
- displayName: config.payments.products[parentId].displayName || parentId, + displayName: config.payments.products[parentId]?.displayName || parentId,
354-358: Use alert instead of toast for blocking validation errors.Per coding guidelines, blocking validation errors should use alerts instead of toasts as they are less easily missed by the user. Replace the toast with an alert.
Based on learnings, for blocking alerts and errors, never use toast; use alerts instead.
452-508: Free trial popover doesn't sync state when opening.When the popover opens,
freeTrialCountandfreeTrialUnitstate (lines 273-274) are never synced from the product's currentfreeTrialvalue. The popover will always show the hardcoded defaults (7 days) instead of the current free trial configuration.Add an
onOpenChangehandler that syncs state when opening:- <Popover open={freeTrialPopoverOpen} onOpenChange={setFreeTrialPopoverOpen}> + <Popover open={freeTrialPopoverOpen} onOpenChange={(open) => { + if (open && freeTrialInfo) { + setFreeTrialCount(freeTrialInfo[0]); + setFreeTrialUnit(freeTrialInfo[1]); + } + setFreeTrialPopoverOpen(open); + }}>
678-696: Missing error handling inhandleDeletePriceswallows failures.The try/finally block clears
deletingPriceIdsbut silently swallows any error fromproject.updateConfig. UserunAsynchronouslyWithAlertor re-throw errors after cleanup.
873-882: Missing delete confirmation for prices.Clicking the delete button immediately deletes the price without confirmation. Add a confirmation dialog to prevent accidental deletions.
1160-1178: Missing error handling inhandleDeleteItemswallows failures.Same try/finally pattern as
handleDeletePrice: errors fromproject.updateConfigare silently swallowed. UserunAsynchronouslyWithAlertor re-throw errors.
1244-1246: Missing null check for item reference.Line 1246 accesses
config.payments.items[itemId].displayNamewithout verifying the item exists. If an item was deleted but remains inincludedItems, this will throw a TypeError.Add optional chaining:
- const displayName = itemConfig.displayName || itemId; + const displayName = itemConfig?.displayName || itemId;
1507-1507: Hard limit of 100 transactions may hide customers.The hard limit of 100 transactions means customers may be hidden if the product has more than 100 purchase transactions. Consider implementing pagination or displaying a warning when the limit is reached.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx(1 hunks)apps/dashboard/src/components/editable-grid.tsx(1 hunks)packages/stack-ui/src/components/ui/input.tsx(2 hunks)packages/stack-ui/src/components/ui/select.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Always add new E2E tests when changing the API or SDK interface
For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Use ES6 maps instead of records wherever you can
Files:
packages/stack-ui/src/components/ui/select.tsxapps/dashboard/src/components/editable-grid.tsxpackages/stack-ui/src/components/ui/input.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,css}: Keep hover/click transitions snappy and fast; avoid fade-in delays on hover. Apply transitions after action completion instead, like smooth fade-out when hover ends
Use hover-exit transitions instead of hover-enter transitions; for example, use 'transition-colors hover:transition-none' instead of fade-in on hover
Files:
packages/stack-ui/src/components/ui/select.tsxapps/dashboard/src/components/editable-grid.tsxpackages/stack-ui/src/components/ui/input.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
{.env*,**/*.{ts,tsx,js}}
📄 CodeRabbit inference engine (AGENTS.md)
Prefix environment variables with STACK_ (or NEXT_PUBLIC_STACK_ if public) so changes are picked up by Turborepo and improves readability
Files:
packages/stack-ui/src/components/ui/select.tsxapps/dashboard/src/components/editable-grid.tsxpackages/stack-ui/src/components/ui/input.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
apps/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static
Files:
apps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
apps/dashboard/**/*
📄 CodeRabbit inference engine (AGENTS.md)
When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Files:
apps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
apps/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Files:
apps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
🧠 Learnings (3)
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Applied to files:
apps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.
Applied to files:
apps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
🧬 Code graph analysis (2)
apps/dashboard/src/components/editable-grid.tsx (4)
apps/e2e/tests/js/convex.test.ts (1)
open(31-33)packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)packages/stack-ui/src/components/ui/select.tsx (5)
Select(160-160)SelectTrigger(160-160)SelectValue(160-160)SelectContent(160-160)SelectItem(160-160)apps/dashboard/src/components/editable-input.tsx (1)
EditableInput(20-145)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (6)
packages/stack-ui/src/components/ui/typography.tsx (1)
Typography(47-47)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (6)
Product(8-8)DEFAULT_INTERVAL_UNITS(16-16)generateUniqueId(139-141)intervalLabel(26-34)PRICE_INTERVAL_UNITS(17-17)shortIntervalLabel(39-45)apps/dashboard/src/components/editable-grid.tsx (2)
EditableGridItem(86-92)EditableGrid(383-399)packages/stack-shared/src/utils/objects.tsx (1)
typedEntries(263-265)apps/dashboard/src/components/repeating-input.tsx (1)
RepeatingInput(79-278)packages/stack-shared/src/utils/numbers.tsx (1)
prettyPrintWithMagnitudes(9-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: Vercel Agent Review
- GitHub Check: Cursor Bugbot
- GitHub Check: build (22.x)
- GitHub Check: setup-tests-with-custom-base-port
- GitHub Check: docker
- GitHub Check: lint_and_build (latest)
- GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
- GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
- GitHub Check: all-good
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: restart-dev-and-test-with-custom-base-port
🔇 Additional comments (8)
packages/stack-ui/src/components/ui/select.tsx (1)
28-28: LGTM! Styling simplification improves performance.Removing
backdrop-blur-mdand the semi-transparent backgrounds simplifies the visual chrome while improving render performance, as backdrop filters are computationally expensive. The component retains all essential styling (borders, spacing, focus states) and functionality.packages/stack-ui/src/components/ui/input.tsx (1)
16-16: LGTM! Visual styling simplification aligns with redesign objectives.The removal of
backdrop-blur-mdand background color classes simplifies the input styling while maintaining the necessary flex layout structure. The wrapper divs are retained appropriately to ensureflex-1expansion behavior works correctly.Also applies to: 30-30
apps/dashboard/src/components/editable-grid.tsx (3)
101-118: LGTM!The
GridLabel,CustomButtonField, andCustomDropdownFieldcomponents are straightforward presentational components with appropriate styling and accessibility considerations.Also applies to: 274-322
325-381: LGTM!The
GridItemValuecomponent provides a clean type-safe routing mechanism for rendering different grid item types.
383-399: LGTM!The
EditableGridcomponent provides a clean, responsive two-column layout with appropriate grid configuration.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (3)
66-89: LGTM!The
PageClientcomponent appropriately handles the not-found case with a clear UI and navigation back to the products list.
1596-1700: LGTM!The customer display components (
CustomerRow,UserCell,TeamCell) appropriately handle loading states and provide clear navigation links.
540-614: LGTM!The dialog components provide clear UX for editing add-ons, prices, and items with appropriate form controls and validation hints.
Also applies to: 915-1083, 1311-1437
| const handleSavePrice = async (editing: EditingPrice, isNew: boolean) => { | ||
| const interval: DayInterval | undefined = editing.intervalSelection === 'one-time' | ||
| ? undefined | ||
| : [editing.intervalCount, editing.priceInterval || 'month']; | ||
|
|
||
| const freeTrial: DayInterval | undefined = editing.freeTrialEnabled | ||
| ? [editing.freeTrialCount, editing.freeTrialUnit] | ||
| : undefined; | ||
|
|
||
| const newPrice: Price = { | ||
| USD: editing.amount, | ||
| serverOnly: !!editing.serverOnly, | ||
| ...(interval && { interval }), | ||
| ...(freeTrial && { freeTrial }), | ||
| }; | ||
|
|
||
| const currentPrices = typeof prices === 'object' && prices !== null ? prices : {}; | ||
| const updatedPrices = { | ||
| ...currentPrices, | ||
| [editing.priceId]: newPrice, | ||
| }; | ||
|
|
||
| await project.updateConfig({ | ||
| [`payments.products.${productId}.prices`]: updatedPrices, | ||
| }); | ||
|
|
||
| toast({ title: isNew ? "Price added" : "Price updated" }); | ||
| setEditingPrice(null); | ||
| setIsAddingPrice(false); | ||
| }; |
There was a problem hiding this comment.
Missing error handling in multiple async handlers.
Multiple async functions (handleSavePrice, handleAddPrices, handleSetIncludeByDefault, handleSetFreeNotIncluded, handleDisplayNameUpdate, handleCatalogUpdate, handleCreateCatalog, handleStackableUpdate, handleServerOnlyUpdate, handleAddOnSave, handleFreeTrialSave, handleRemoveFreeTrial) lack error handling. If any project.updateConfig call fails, the error will be unhandled.
Per coding guidelines and learnings, these handlers should use runAsynchronouslyWithAlert from stackframe/stack-shared/dist/utils/promises to automatically handle errors and show alerts to users.
Based on learnings, use runAsynchronouslyWithAlert for async button click handlers and form submissions.
Also applies to: 736-760, 312-385
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
around lines 647-676 (and also apply same changes at 736-760 and 312-385), the
async handlers (handleSavePrice and the other handlers listed in the comment)
currently call project.updateConfig directly and lack error handling; update
each async click/form handler to be invoked via runAsynchronouslyWithAlert from
stackframe/stack-shared/dist/utils/promises (import it at the top of the file),
i.e. wrap the existing async body in a function passed to
runAsynchronouslyWithAlert so any thrown error is caught and an alert shown,
keep existing success toast/state updates inside the wrapped function, and
ensure the import and usage follow project conventions for other handlers.
| setEditingItem(null); | ||
| setIsAddingItem(false); | ||
| setSelectedItemId(''); | ||
| }; |
There was a problem hiding this comment.
Bug: Included item repeat uses incompatible sentinel value
handleSaveItem writes repeat: 'once' for non-repeating items, but other payments code (e.g., shortIntervalLabel) models non-repeating as 'never'. Persisting 'once' can produce incorrect interval rendering and inconsistent config state for included items.
There was a problem hiding this comment.
Bug: Disabled email sending permanently marks outbox finished
When STACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING is enabled, the code returns a Result.error and then updates the row with finishedSendingAt. This causes queued emails to be treated as completed failures and never retried/sent later when the flag is turned off, which conflicts with “disable sending” semantics.
apps/backend/src/lib/email-queue-step.tsx#L499-L530
stack-auth/apps/backend/src/lib/email-queue-step.tsx
Lines 499 to 530 in 50235b7
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx (1)
377-377: Verify product ID character restrictions.The input filter at line 377 removes underscores (
/[^a-z0-9-]/g), but the validation pattern at line 120 is/^[a-z0-9-]+$/. According to a past review comment, items allow underscores (seen in item-dialog.tsx). This creates an inconsistency between product and item ID formats.Consider either:
- Allowing underscores in product IDs to match items:
.replace(/[^a-z0-9_-]/g, '')- Or documenting why products restrict underscores while items allow them
#!/bin/bash # Check if item IDs allow underscores in the codebase rg -n "item.*id.*pattern|item.*validation" -A 3 -B 3 --type ts --type tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (2)
447-458: React hook called inside event handler.Line 449 calls
project.useConfig()inside an asynconClickhandler, violating React's Rules of Hooks. Additionally, per coding guidelines, async handlers should userunAsynchronouslyWithAlertfor proper error handling.Move the config call to component level and wrap the handler:
function ProductsList({ ... }: ProductsListProps) { const stackAdminApp = useAdminApp(); const project = stackAdminApp.useProject(); + const config = project.useConfig(); const projectId = useProjectId(); const router = useRouter(); ... okButton={{ label: "Delete", - onClick: async () => { + onClick: () => runAsynchronouslyWithAlert(async () => { if (!productToDelete) return; - const config = project.useConfig(); const updatedProducts = typedFromEntries( typedEntries(config.payments.products) .filter(([productId]) => productId !== productToDelete.id) ); await project.updateConfig({ "payments.products": updatedProducts }); toast({ title: "Product deleted" }); setProductToDelete(null); - } + }) }}Based on learnings, use
runAsynchronouslyWithAlertfrom@stackframe/stack-shared/dist/utils/promisesfor async button handlers.
584-590: React hook pattern issue and missing error handling.Similar to the product deletion handler, this should use
runAsynchronouslyWithAlertfor proper error handling.Apply this fix:
onClick: () => runAsynchronouslyWithAlert(async () => { if (!itemToDelete) return; await project.updateConfig({ [`payments.items.${itemToDelete.id}`]: null }); toast({ title: "Item deleted" }); setItemToDelete(null); })Import the utility:
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";Based on learnings, async handlers should use
runAsynchronouslyWithAlertinstead of raw async functions.
🧹 Nitpick comments (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx (2)
217-234: Extract responsive breakpoint to a named constant.The magic number
900for the preview width threshold reduces readability.Apply this diff:
+const PREVIEW_MIN_WIDTH = 900; + export default function PageClient() { // ... useLayoutEffect(() => { const container = mainContentRef.current; if (!container) return; const updateShowPreview = () => { - setShowPreview(container.offsetWidth > 900); + setShowPreview(container.offsetWidth > PREVIEW_MIN_WIDTH); };
237-251: Consider memoizing computed product and item lists.
existingProductsandexistingItemsare recomputed on every render. While the performance impact is likely minimal for small datasets, memoizing these values follows best practices and prevents unnecessary array allocations.Apply this diff:
+import { useLayoutEffect, useRef, useState, useMemo } from "react"; + export default function PageClient() { // ... state declarations ... // Computed values - const existingProducts = typedEntries(paymentsConfig.products) + const existingProducts = useMemo(() => typedEntries(paymentsConfig.products) .map(([id, product]) => ({ id, displayName: product.displayName, catalogId: product.catalogId, customerType: product.customerType - })); + })), [paymentsConfig.products]); - const existingItems = typedEntries(paymentsConfig.items).map(([id, item]) => ({ + const existingItems = useMemo(() => typedEntries(paymentsConfig.items).map(([id, item]) => ({ id, displayName: item.displayName, customerType: item.customerType - })); + })), [paymentsConfig.items]);
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx(4 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx(18 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx(3 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx(11 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
- apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Always add new E2E tests when changing the API or SDK interface
For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Use ES6 maps instead of records wherever you can
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx
**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,css}: Keep hover/click transitions snappy and fast; avoid fade-in delays on hover. Apply transitions after action completion instead, like smooth fade-out when hover ends
Use hover-exit transitions instead of hover-enter transitions; for example, use 'transition-colors hover:transition-none' instead of fade-in on hover
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx
apps/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx
{.env*,**/*.{ts,tsx,js}}
📄 CodeRabbit inference engine (AGENTS.md)
Prefix environment variables with STACK_ (or NEXT_PUBLIC_STACK_ if public) so changes are picked up by Turborepo and improves readability
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx
apps/dashboard/**/*
📄 CodeRabbit inference engine (AGENTS.md)
When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx
apps/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx
🧠 Learnings (6)
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/**/*.tsx : Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/dashboard/**/* : When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/**/*.{ts,tsx} : NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
🧬 Code graph analysis (6)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (3)
Product(8-8)Price(9-9)generateUniqueId(155-157)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
PageClient(66-87)packages/stack-shared/src/config/schema.ts (1)
CompleteConfig(1074-1074)packages/stack-shared/src/utils/objects.tsx (1)
typedEntries(263-265)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsx (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (1)
PageClient(73-96)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx (1)
PageClient(176-1023)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (1)
PageClient(598-945)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
PageClient(66-87)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsx (3)
apps/dashboard/src/components/repeating-input.tsx (1)
RepeatingInput(79-278)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (3)
PRICE_INTERVAL_UNITS(17-17)DEFAULT_INTERVAL_UNITS(16-16)Price(9-9)apps/dashboard/src/components/editable-grid.tsx (1)
EditableGrid(383-399)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsx (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (3)
Price(9-9)generateUniqueId(155-157)formatPriceDisplay(111-122)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsx (5)
EditingPrice(30-40)editingPriceToPrice(283-298)createNewEditingPrice(266-278)priceToEditingPrice(249-261)PriceEditDialog(51-244)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsx (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (6)
Product(8-8)PricesObject(10-10)intervalLabel(26-34)freeTrialLabel(50-55)getPricesObject(127-137)shortIntervalLabel(39-45)packages/stack-shared/src/utils/objects.tsx (1)
typedEntries(263-265)packages/stack-shared/src/utils/numbers.tsx (1)
prettyPrintWithMagnitudes(9-26)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (1)
ListSection(19-104)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsx (1)
PricingSection(30-301)
🪛 Biome (2.1.2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
[error] 449-449: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: Vercel Agent Review
- GitHub Check: Cursor Bugbot
- GitHub Check: lint_and_build (latest)
- GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
- GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
- GitHub Check: all-good
- GitHub Check: setup-tests
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: restart-dev-and-test
- GitHub Check: docker
- GitHub Check: setup-tests-with-custom-base-port
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
🔇 Additional comments (11)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (2)
63-98: LGTM: Well-structured nullable handling.The updated
buildPriceUpdatecorrectly handles three distinct cases forfreeTrial: explicit removal (null), explicit value (DayInterval), and computed from selection state (undefined). The JSDoc clarifies the semantics well.
108-122: LGTM: Clean price formatting utility.The
formatPriceDisplayhelper provides consistent price formatting across the UI. Pluralization logic is correct.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page.tsx (1)
1-10: LGTM: Standard Next.js page pattern.The page correctly exports metadata and renders the PageClient component. Based on learnings, here's a deep link to test the new product creation page:
http://localhost:8101/projects/-selector-/payments/products/newapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (1)
410-410: LGTM: Router navigation for product details.The onClick handler correctly navigates to the product detail page using the router. Based on learnings, here's the deep link pattern:
http://localhost:8101/projects/-selector-/payments/products/{productId}apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsx (1)
1-180: LGTM: Well-structured preview component.The
ProductCardPreviewcomponent provides a clean card-based preview of products with pricing, badges, and included items. The component correctly uses utility functions for formatting and handles edge cases (no prices, no items) gracefully.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/price-edit-dialog.tsx (1)
1-299: LGTM: Comprehensive price editing dialog.The
PriceEditDialogcomponent and its helper functions provide a solid foundation for price editing. The conversion functions (priceToEditingPrice,createNewEditingPrice,editingPriceToPrice) correctly transform between domain and UI representations.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/pricing-section.tsx (1)
1-301: LGTM: Flexible pricing section component.The
PricingSectioncomponent elegantly handles both dialog and form variants, plus a special free-product state. State management is clean with proper separation between editing state (editingPrice) and persisted prices.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (2)
12-64: LGTM: Streamlined welcome screen with proper navigation.The updated
WelcomeScreencorrectly uses theprojectIdprop andLinkcomponent for navigation to the new product page. This follows Next.js best practices by avoiding dynamic functions and keeping the page static.Based on learnings, here's the deep link to the products page:
http://localhost:8101/projects/-selector-/payments/products
66-87: LGTM: Simplified page logic.The refactored
PageClientcorrectly usesuseProjectIdand removes the previous multi-view complexity. The conditional rendering (WelcomeScreen vs PageClientListView) is clean and appropriate.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx (1)
627-633: LGTM: Clean PricingSection integration.The replacement of inline price management with the
PricingSectioncomponent simplifies the dialog and centralizes pricing logic. Thevariant="dialog"prop is used appropriately.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx (1)
126-126: LGTM! Excellent use of hover-exit transitions.The transition patterns correctly follow the coding guidelines by using
"transition-all duration-150 hover:transition-none", which applies transitions on hover-exit rather than hover-enter. This keeps interactions snappy and responsive.As per coding guidelines: "Use hover-exit transitions instead of hover-enter transitions."
Also applies to: 591-591, 626-626, 708-708
| function CustomerTypeSelection({ | ||
| onSelectCustomerType, | ||
| onCancel, | ||
| }: { | ||
| onSelectCustomerType: (type: 'user' | 'team' | 'custom') => void, | ||
| onCancel: () => void, | ||
| }) { | ||
| return ( | ||
| <div className="flex flex-col h-full"> | ||
| <div className="flex items-center gap-4 px-6 py-4 border-b border-border/40"> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={onCancel} | ||
| className="gap-2" | ||
| > | ||
| <ArrowLeft className="h-4 w-4" /> | ||
| Back | ||
| </Button> | ||
| <Typography type="h3" className="font-semibold">Create Product</Typography> | ||
| </div> | ||
| <div className="flex-1 flex items-center justify-center p-6"> | ||
| <div className="space-y-4 max-w-md mx-auto"> | ||
| <div className="text-center mb-8"> | ||
| <Typography type="h2" className="text-2xl font-semibold">Who will this product be for?</Typography> | ||
| </div> | ||
|
|
||
| <div className="grid gap-3"> | ||
| {CUSTOMER_TYPE_OPTIONS.map((option) => { | ||
| const Icon = option.icon; | ||
| const colorClasses = { | ||
| blue: { | ||
| hover: 'hover:border-blue-500/40 hover:shadow-[0_0_12px_rgba(59,130,246,0.1)]', | ||
| bg: 'bg-blue-500/10 dark:bg-blue-500/[0.15] group-hover:bg-blue-500/20', | ||
| icon: 'text-blue-600 dark:text-blue-400', | ||
| }, | ||
| emerald: { | ||
| hover: 'hover:border-emerald-500/40 hover:shadow-[0_0_12px_rgba(16,185,129,0.1)]', | ||
| bg: 'bg-emerald-500/10 dark:bg-emerald-500/[0.15] group-hover:bg-emerald-500/20', | ||
| icon: 'text-emerald-600 dark:text-emerald-400', | ||
| }, | ||
| amber: { | ||
| hover: 'hover:border-amber-500/40 hover:shadow-[0_0_12px_rgba(245,158,11,0.1)]', | ||
| bg: 'bg-amber-500/10 dark:bg-amber-500/[0.15] group-hover:bg-amber-500/20', | ||
| icon: 'text-amber-600 dark:text-amber-400', | ||
| }, | ||
| }[option.color]; | ||
|
|
||
| return ( | ||
| <Card | ||
| key={option.value} | ||
| className={cn( | ||
| "cursor-pointer group", | ||
| "rounded-xl border border-border/50 dark:border-foreground/[0.1]", | ||
| "bg-foreground/[0.02] hover:bg-foreground/[0.04]", | ||
| colorClasses.hover, | ||
| "transition-all duration-150 hover:transition-none" | ||
| )} | ||
| onClick={() => onSelectCustomerType(option.value)} | ||
| > | ||
| <CardHeader className="p-4"> | ||
| <div className="flex items-center gap-3"> | ||
| <div className={cn( | ||
| "p-2.5 rounded-xl transition-colors duration-150 group-hover:transition-none", | ||
| colorClasses.bg | ||
| )}> | ||
| <Icon className={cn("h-5 w-5", colorClasses.icon)} /> | ||
| </div> | ||
| <div> | ||
| <CardTitle className="text-base font-semibold">{option.label}</CardTitle> | ||
| <CardDescription className="text-sm mt-1 text-muted-foreground"> | ||
| {option.description} | ||
| </CardDescription> | ||
| </div> | ||
| </div> | ||
| </CardHeader> | ||
| </Card> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Consider keyboard accessibility for customer type cards.
The customer type selection cards are clickable via onClick but lack keyboard navigation support. Screen reader users and keyboard-only users cannot easily interact with these cards.
Apply this diff to improve accessibility:
<Card
key={option.value}
+ role="button"
+ tabIndex={0}
className={cn(
"cursor-pointer group",
"rounded-xl border border-border/50 dark:border-foreground/[0.1]",
"bg-foreground/[0.02] hover:bg-foreground/[0.04]",
colorClasses.hover,
"transition-all duration-150 hover:transition-none"
)}
onClick={() => onSelectCustomerType(option.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onSelectCustomerType(option.value);
+ }
+ }}
>📝 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.
| function CustomerTypeSelection({ | |
| onSelectCustomerType, | |
| onCancel, | |
| }: { | |
| onSelectCustomerType: (type: 'user' | 'team' | 'custom') => void, | |
| onCancel: () => void, | |
| }) { | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <div className="flex items-center gap-4 px-6 py-4 border-b border-border/40"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onCancel} | |
| className="gap-2" | |
| > | |
| <ArrowLeft className="h-4 w-4" /> | |
| Back | |
| </Button> | |
| <Typography type="h3" className="font-semibold">Create Product</Typography> | |
| </div> | |
| <div className="flex-1 flex items-center justify-center p-6"> | |
| <div className="space-y-4 max-w-md mx-auto"> | |
| <div className="text-center mb-8"> | |
| <Typography type="h2" className="text-2xl font-semibold">Who will this product be for?</Typography> | |
| </div> | |
| <div className="grid gap-3"> | |
| {CUSTOMER_TYPE_OPTIONS.map((option) => { | |
| const Icon = option.icon; | |
| const colorClasses = { | |
| blue: { | |
| hover: 'hover:border-blue-500/40 hover:shadow-[0_0_12px_rgba(59,130,246,0.1)]', | |
| bg: 'bg-blue-500/10 dark:bg-blue-500/[0.15] group-hover:bg-blue-500/20', | |
| icon: 'text-blue-600 dark:text-blue-400', | |
| }, | |
| emerald: { | |
| hover: 'hover:border-emerald-500/40 hover:shadow-[0_0_12px_rgba(16,185,129,0.1)]', | |
| bg: 'bg-emerald-500/10 dark:bg-emerald-500/[0.15] group-hover:bg-emerald-500/20', | |
| icon: 'text-emerald-600 dark:text-emerald-400', | |
| }, | |
| amber: { | |
| hover: 'hover:border-amber-500/40 hover:shadow-[0_0_12px_rgba(245,158,11,0.1)]', | |
| bg: 'bg-amber-500/10 dark:bg-amber-500/[0.15] group-hover:bg-amber-500/20', | |
| icon: 'text-amber-600 dark:text-amber-400', | |
| }, | |
| }[option.color]; | |
| return ( | |
| <Card | |
| key={option.value} | |
| className={cn( | |
| "cursor-pointer group", | |
| "rounded-xl border border-border/50 dark:border-foreground/[0.1]", | |
| "bg-foreground/[0.02] hover:bg-foreground/[0.04]", | |
| colorClasses.hover, | |
| "transition-all duration-150 hover:transition-none" | |
| )} | |
| onClick={() => onSelectCustomerType(option.value)} | |
| > | |
| <CardHeader className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className={cn( | |
| "p-2.5 rounded-xl transition-colors duration-150 group-hover:transition-none", | |
| colorClasses.bg | |
| )}> | |
| <Icon className={cn("h-5 w-5", colorClasses.icon)} /> | |
| </div> | |
| <div> | |
| <CardTitle className="text-base font-semibold">{option.label}</CardTitle> | |
| <CardDescription className="text-sm mt-1 text-muted-foreground"> | |
| {option.description} | |
| </CardDescription> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| </Card> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function CustomerTypeSelection({ | |
| onSelectCustomerType, | |
| onCancel, | |
| }: { | |
| onSelectCustomerType: (type: 'user' | 'team' | 'custom') => void, | |
| onCancel: () => void, | |
| }) { | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <div className="flex items-center gap-4 px-6 py-4 border-b border-border/40"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onCancel} | |
| className="gap-2" | |
| > | |
| <ArrowLeft className="h-4 w-4" /> | |
| Back | |
| </Button> | |
| <Typography type="h3" className="font-semibold">Create Product</Typography> | |
| </div> | |
| <div className="flex-1 flex items-center justify-center p-6"> | |
| <div className="space-y-4 max-w-md mx-auto"> | |
| <div className="text-center mb-8"> | |
| <Typography type="h2" className="text-2xl font-semibold">Who will this product be for?</Typography> | |
| </div> | |
| <div className="grid gap-3"> | |
| {CUSTOMER_TYPE_OPTIONS.map((option) => { | |
| const Icon = option.icon; | |
| const colorClasses = { | |
| blue: { | |
| hover: 'hover:border-blue-500/40 hover:shadow-[0_0_12px_rgba(59,130,246,0.1)]', | |
| bg: 'bg-blue-500/10 dark:bg-blue-500/[0.15] group-hover:bg-blue-500/20', | |
| icon: 'text-blue-600 dark:text-blue-400', | |
| }, | |
| emerald: { | |
| hover: 'hover:border-emerald-500/40 hover:shadow-[0_0_12px_rgba(16,185,129,0.1)]', | |
| bg: 'bg-emerald-500/10 dark:bg-emerald-500/[0.15] group-hover:bg-emerald-500/20', | |
| icon: 'text-emerald-600 dark:text-emerald-400', | |
| }, | |
| amber: { | |
| hover: 'hover:border-amber-500/40 hover:shadow-[0_0_12px_rgba(245,158,11,0.1)]', | |
| bg: 'bg-amber-500/10 dark:bg-amber-500/[0.15] group-hover:bg-amber-500/20', | |
| icon: 'text-amber-600 dark:text-amber-400', | |
| }, | |
| }[option.color]; | |
| return ( | |
| <Card | |
| key={option.value} | |
| role="button" | |
| tabIndex={0} | |
| className={cn( | |
| "cursor-pointer group", | |
| "rounded-xl border border-border/50 dark:border-foreground/[0.1]", | |
| "bg-foreground/[0.02] hover:bg-foreground/[0.04]", | |
| colorClasses.hover, | |
| "transition-all duration-150 hover:transition-none" | |
| )} | |
| onClick={() => onSelectCustomerType(option.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| onSelectCustomerType(option.value); | |
| } | |
| }} | |
| > | |
| <CardHeader className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className={cn( | |
| "p-2.5 rounded-xl transition-colors duration-150 group-hover:transition-none", | |
| colorClasses.bg | |
| )}> | |
| <Icon className={cn("h-5 w-5", colorClasses.icon)} /> | |
| </div> | |
| <div> | |
| <CardTitle className="text-base font-semibold">{option.label}</CardTitle> | |
| <CardDescription className="text-sm mt-1 text-muted-foreground"> | |
| {option.description} | |
| </CardDescription> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| </Card> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx
around lines 70 to 154, the customer type Card elements are only clickable via
onClick and are not keyboard accessible; make each Card focusable and operable
via keyboard by adding tabIndex={0}, role="button", and an onKeyDown handler
that triggers onSelectCustomerType when Enter or Space is pressed; also include
an accessible name (aria-label or ensure option.label is announced) and preserve
the existing onClick behavior so both mouse and keyboard activate the same
action.
| const handleSave = async () => { | ||
| const validationErrors = validateForm(); | ||
| if (Object.keys(validationErrors).length > 0) { | ||
| setErrors(validationErrors); | ||
| return; | ||
| } | ||
|
|
||
| setIsSaving(true); | ||
| try { | ||
| const product: Product = { | ||
| displayName, | ||
| customerType, | ||
| catalogId: catalogId || undefined, | ||
| isAddOnTo: isAddOn ? Object.fromEntries(isAddOnTo.map(id => [id, true])) : false, | ||
| stackable, | ||
| prices: freeByDefault ? 'include-by-default' : prices, | ||
| includedItems, | ||
| serverOnly, | ||
| freeTrial, | ||
| }; | ||
|
|
||
| await project.updateConfig({ [`payments.products.${productId}`]: product }); | ||
| toast({ title: "Product created" }); | ||
| router.push(`/projects/${projectId}/payments/products`); | ||
| } catch (error) { | ||
| toast({ title: "Failed to create product", variant: "destructive" }); | ||
| } finally { | ||
| setIsSaving(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Replace toast with alert for blocking errors.
Line 353 uses toast for error feedback in handleSave, but the coding guidelines specify: "For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user." Additionally, the try-catch pattern swallows detailed error information that could help with debugging.
As per coding guidelines, errors should use alerts or runAsynchronouslyWithAlert.
Apply this diff to use the recommended error handling pattern:
const handleSave = async () => {
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setIsSaving(true);
- try {
+ runAsynchronouslyWithAlert(async () => {
const product: Product = {
displayName,
customerType,
catalogId: catalogId || undefined,
isAddOnTo: isAddOn ? Object.fromEntries(isAddOnTo.map(id => [id, true])) : false,
stackable,
prices: freeByDefault ? 'include-by-default' : prices,
includedItems,
serverOnly,
freeTrial,
};
await project.updateConfig({ [`payments.products.${productId}`]: product });
toast({ title: "Product created" });
router.push(`/projects/${projectId}/payments/products`);
- } catch (error) {
- toast({ title: "Failed to create product", variant: "destructive" });
+ }, {
+ finally: () => {
+ setIsSaving(false);
+ }
- } finally {
- setIsSaving(false);
- }
+ });
};🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx
around lines 328 to 357, replace the non-blocking toast used in the catch with
the project standard blocking alert pattern: wrap the async save in
runAsynchronouslyWithAlert or, if keeping the try/catch, call the alert API (or
runAsynchronouslyWithAlert) with a clear title and include the caught error
message/details; also ensure the caught error is logged (console.error or
process logger) for debugging and preserve the finally block that sets
setIsSaving(false).
| {isAddOn && ( | ||
| <div className="mt-1.5 mb-0.5 space-y-1 p-2 rounded-lg bg-foreground/[0.02] border border-border/30"> | ||
| <span className="text-xs text-foreground/50">Add-on to:</span> | ||
| <div className="space-y-1 max-h-24 overflow-y-auto"> | ||
| {existingProducts.filter(o => !o.id.startsWith('addon')).map(product => ( | ||
| <label key={product.id} className="flex items-center gap-2 cursor-pointer"> | ||
| <Checkbox | ||
| id={`addon-to-${product.id}`} | ||
| checked={isAddOnTo.includes(product.id)} | ||
| onCheckedChange={(checked) => { | ||
| if (checked) { | ||
| setIsAddOnTo(prev => [...prev, product.id]); | ||
| } else { | ||
| setIsAddOnTo(prev => prev.filter(id => id !== product.id)); | ||
| } | ||
| if (errors.isAddOnTo) { | ||
| setErrors(prev => { | ||
| const newErrors = { ...prev }; | ||
| delete newErrors.isAddOnTo; | ||
| return newErrors; | ||
| }); | ||
| } | ||
| }} | ||
| /> | ||
| <span className="text-sm">{product.displayName}</span> | ||
| </label> | ||
| ))} | ||
| </div> | ||
| {errors.isAddOnTo && ( | ||
| <Typography type="label" className="text-destructive text-xs"> | ||
| {errors.isAddOnTo} | ||
| </Typography> | ||
| )} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </> |
There was a problem hiding this comment.
Validate customer type consistency for add-ons.
The add-on selection logic (line 811) filters products by checking if their ID starts with 'addon', but there's no validation ensuring that the selected add-on products have a compatible customer type with the current product. This could lead to data integrity issues where add-ons and their parent products have mismatched customer types.
Consider adding this validation in the validateForm function:
if (isAddOn && isAddOnTo.length > 0) {
const addOnCatalogs = new Set(
isAddOnTo.map(pid => existingProducts.find(o => o.id === pid)?.catalogId)
);
if (addOnCatalogs.size > 1) {
newErrors.isAddOnTo = "All selected products must be in the same catalog";
}
+
+ // Validate customer type compatibility
+ const incompatibleProducts = isAddOnTo.filter(pid => {
+ const product = existingProducts.find(o => o.id === pid);
+ return product && product.customerType !== customerType;
+ });
+ if (incompatibleProducts.length > 0) {
+ newErrors.isAddOnTo = "Add-on products must have the same customer type as the selected products";
+ }
}Additionally, filter the product list to only show compatible products:
<div className="space-y-1 max-h-24 overflow-y-auto">
- {existingProducts.filter(o => !o.id.startsWith('addon')).map(product => (
+ {existingProducts.filter(o => !o.id.startsWith('addon') && o.customerType === customerType).map(product => (
<label key={product.id} className="flex items-center gap-2 cursor-pointer">Committable suggestion skipped: line range outside the PR's diff.
| <div className="mt-1.5 mb-0.5 space-y-1 p-2 rounded-lg bg-foreground/[0.02] border border-border/30"> | ||
| <span className="text-xs text-foreground/50">Add-on to:</span> | ||
| <div className="space-y-1 max-h-24 overflow-y-auto"> | ||
| {existingProducts.filter(o => !o.id.startsWith('addon')).map(product => ( |
There was a problem hiding this comment.
Document or replace magic string 'addon' prefix convention.
Line 811 filters products using .filter(o => !o.id.startsWith('addon')), which relies on an undocumented naming convention. This assumption is fragile and could break if product IDs don't follow the expected pattern.
Consider one of these approaches:
- Add a field to Product type to explicitly mark add-ons:
// In utils.ts or schema
export function isAddOnProduct(product: Product): boolean {
return typeof product.isAddOnTo === 'object' && Object.keys(product.isAddOnTo).length > 0;
}- Document the convention with a comment:
// Filter out add-on products (conventionally prefixed with 'addon-')
{existingProducts.filter(o => !o.id.startsWith('addon')).map(product => (The first approach is more robust and type-safe.
| <IncludedItemDialog | ||
| open={showItemDialog} | ||
| onOpenChange={setShowItemDialog} | ||
| existingItems={existingItems} | ||
| existingIncludedItemIds={Object.keys(includedItems)} | ||
| editingItemId={editingItemId} | ||
| editingItem={editingItemId ? includedItems[editingItemId] : undefined} | ||
| onSave={(itemId, item) => { | ||
| if (editingItemId) { | ||
| editIncludedItem(itemId, item); | ||
| } else { | ||
| addIncludedItem(itemId, item); | ||
| } | ||
| }} | ||
| onCreateNewItem={() => setShowNewItemDialog(true)} | ||
| /> | ||
|
|
||
| <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} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find IncludedItemDialog component
find . -type f -name "*.tsx" -o -name "*.ts" | xargs grep -l "IncludedItemDialog" | head -20Repository: stack-auth/stack-auth
Length of output: 377
🏁 Script executed:
#!/bin/bash
# Read the IncludedItemDialog component file
cat -n ./apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/included-item-dialog.tsxRepository: stack-auth/stack-auth
Length of output: 16380
🏁 Script executed:
#!/bin/bash
# Read the page-client.tsx around lines 994-1020 and context
wc -l ./apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/new/page-client.tsxRepository: stack-auth/stack-auth
Length of output: 172
🏁 Script executed:
#!/bin/bash
# Read the page-client.tsx to understand the context around lines 994-1020
sed -n '950,1023p' ./apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/new/page-client.tsxRepository: stack-auth/stack-auth
Length of output: 2579
🏁 Script executed:
#!/bin/bash
# Search for existingItems definition and customerType in the file
sed -n '1,300p' ./apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/new/page-client.tsx | grep -n "existingItems\|customerType" | head -30Repository: stack-auth/stack-auth
Length of output: 601
🏁 Script executed:
#!/bin/bash
# Read more context around customerType and existingItems construction
sed -n '180,270p' ./apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/new/page-client.tsxRepository: stack-auth/stack-auth
Length of output: 3336
🏁 Script executed:
#!/bin/bash
# Search for addIncludedItem and editIncludedItem handlers to see if there's validation
grep -n "addIncludedItem\|editIncludedItem" ./apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/new/page-client.tsx | head -20Repository: stack-auth/stack-auth
Length of output: 306
🏁 Script executed:
#!/bin/bash
# Read the addIncludedItem and editIncludedItem handler implementations
sed -n '359,370p' ./apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/new/page-client.tsxRepository: stack-auth/stack-auth
Length of output: 403
Verify customer type enforcement in included item selection.
The IncludedItemDialog displays all items from existingItems regardless of customer type, while ItemDialog enforces forceCustomerType={customerType} for new items. There is no filtering or validation to prevent selecting items with mismatched customer types. Filter existingItems by the current product's customerType before passing to IncludedItemDialog, or add validation in the component to prevent mismatched selections.
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx
around lines 994 to 1020, IncludedItemDialog is being fed existingItems without
enforcing the product's customerType so users can pick items with a mismatched
customerType; update the code to supply only items whose customerType matches
the current product (e.g., filter existingItems by item.customerType ===
customerType) before passing to IncludedItemDialog, and/or add a guard in
IncludedItemDialog to validate selected items’ customerType and reject or show
an error for mismatched selections; ensure existingIncludedItemIds and
editingItem lookups are based on the filtered list so indices/ids remain
consistent.
| setFreeTrialPopoverOpen(false); | ||
| toast({ title: "Free trial removed" }); | ||
| }; | ||
|
|
There was a problem hiding this comment.
Bug: Free trial editor ignores current value
The free trial popover initializes freeTrialCount/freeTrialUnit to 7 day and never syncs them from product.freeTrial. Opening the editor for an existing free trial and clicking “Save” can overwrite it with the default value even if the user didn’t change anything.
There was a problem hiding this comment.
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/create-catalog-dialog.tsx (1)
117-119: Add loading state during async operations.When
onCreateis async, the "Create Catalog" button should be disabled and show a loading indicator to prevent duplicate submissions and provide visual feedback to the user.Add a loading state:
export function CreateCatalogDialog({ open, onOpenChange, onCreate }: CreateCatalogDialogProps) { const [catalogId, setCatalogId] = useState(""); const [displayName, setDisplayName] = useState(""); const [errors, setErrors] = useState<{ id?: string, displayName?: string }>({}); + const [isCreating, setIsCreating] = useState(false);const validateAndCreate = () => { const newErrors: { id?: string, displayName?: string } = {}; // Validate catalog ID if (!catalogId.trim()) { newErrors.id = "Catalog ID is required"; } else if (!/^[a-z0-9-]+$/.test(catalogId)) { newErrors.id = "Catalog ID must contain only lowercase letters, numbers, and hyphens"; } // Validate display name if (!displayName.trim()) { newErrors.displayName = "Display name is required"; } if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } + setIsCreating(true); runAsynchronouslyWithAlert(async () => { await onCreate({ id: catalogId.trim(), displayName: displayName.trim() }); // Reset form only after successful creation setCatalogId(""); setDisplayName(""); setErrors({}); onOpenChange(false); + }, { + onFinally: () => setIsCreating(false), }); };- <DialogFooter> - <Button variant="outline" onClick={handleClose}> + <DialogFooter> + <Button variant="outline" onClick={handleClose} disabled={isCreating}> Cancel </Button> - <Button onClick={validateAndCreate}> + <Button onClick={validateAndCreate} disabled={isCreating}> - Create Catalog + {isCreating ? "Creating..." : "Create Catalog"} </Button> </DialogFooter>
♻️ Duplicate comments (12)
apps/dashboard/src/components/editable-grid.tsx (2)
137-158: UserunAsynchronouslyWithAlertfor user-facing error handling.The current implementation uses
runAsynchronously, which logs errors but doesn't show alerts to users. WhenonUpdatefails, users won't know why their change wasn't saved. Per coding guidelines and learnings, userunAsynchronouslyWithAlertfor form submissions and user-triggered updates.Apply this diff:
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; ... <Select value={value ? 'true' : 'false'} - onValueChange={(v) => runAsynchronously(handleChange(v))} + onValueChange={(v) => runAsynchronouslyWithAlert(handleChange(v))} disabled={isUpdating} >Based on learnings,
runAsynchronouslyWithAlertautomatically handles errors and shows alerts to users.
196-219: UserunAsynchronouslyWithAlertfor user-facing error handling.Same issue as
EditableBooleanField: errors fromonUpdateare logged but not shown to users. When dropdown updates fail, users won't receive feedback.Apply this diff:
<Select value={value} - onValueChange={(v) => runAsynchronously(handleChange(v))} + onValueChange={(v) => runAsynchronouslyWithAlert(handleChange(v))} disabled={isUpdating} >apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (7)
279-281: Sync free trial state when popover opens.The free trial popover always shows defaults (7 days) instead of the current value. When opening, the local state should be initialized from
product.freeTrial.Consider adding a handler to sync state when the popover opens:
+ // Sync state when popover opens + const handleFreeTrialPopoverOpen = (open: boolean) => { + if (open && product.freeTrial) { + setFreeTrialCount(product.freeTrial[0]); + setFreeTrialUnit(product.freeTrial[1]); + } + setFreeTrialPopoverOpen(open); + };Then update line 459:
- <Popover open={freeTrialPopoverOpen} onOpenChange={setFreeTrialPopoverOpen}> + <Popover open={freeTrialPopoverOpen} onOpenChange={handleFreeTrialPopoverOpen}>
106-106: Add null check for catalog reference.If the referenced catalog has been deleted but
catalogIdremains, accessing.displayNamewill throw a TypeError and crash the page.Apply this diff to add a safety check:
- const catalogName = catalogId ? config.payments.catalogs[catalogId].displayName || catalogId : null; + const catalogName = catalogId ? config.payments.catalogs[catalogId]?.displayName || catalogId : null;
143-143: Add null checks for referenced products and items.Multiple locations access config entity properties without verifying the entity exists. If a referenced product, catalog, or item is deleted but references remain (stale data), these will throw TypeErrors:
- Line 143:
config.payments.products[parentId].displayName- Line 1046:
config.payments.items[itemId].displayNameApply optional chaining to prevent crashes:
- displayName: config.payments.products[parentId].displayName || parentId, + displayName: config.payments.products[parentId]?.displayName || parentId,And at line 1046:
- const displayName = itemConfig.displayName || itemId; + const displayName = itemConfig?.displayName || itemId;
361-376: Use alert for blocking validation error.Per coding guidelines, blocking errors should use alerts instead of toasts as they are less easily missed by users.
Based on learnings, ...
Replace the toast with an alert:
const handleAddOnSave = async () => { if (isAddOn && selectedAddOnProducts.size === 0) { - toast({ title: "Please select at least one product", variant: "destructive" }); + alert("Please select at least one product"); return; }
319-392: Add error handling to async handlers.Multiple async handlers (
handleDisplayNameUpdate,handleCatalogUpdate,handleCreateCatalog,handleStackableUpdate,handleServerOnlyUpdate,handleAddOnSave,handleFreeTrialSave,handleRemoveFreeTrial) lack error handling. Ifproject.updateConfigfails, errors will be unhandled.Based on learnings, use
runAsynchronouslyWithAlertfor async handlers:+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";Then wrap each handler. Example for
handleDisplayNameUpdate:- const handleDisplayNameUpdate = async (value: string) => { + const handleDisplayNameUpdate = (value: string) => runAsynchronouslyWithAlert(async () => { await project.updateConfig({ [`payments.products.${productId}.displayName`]: value || null, }); toast({ title: "Display name updated" }); - }; + });Apply the same pattern to all other async handlers in this section.
657-675: Missing error handling in price deletion.The try/finally block clears
deletingPriceIdseven if deletion fails, but errors are silently swallowed.Based on learnings, ...
Wrap the handler with
runAsynchronouslyWithAlert:- const handleDeletePrice = async (priceId: string) => { + const handleDeletePrice = (priceId: string) => runAsynchronouslyWithAlert(async () => { setDeletingPriceIds(prev => new Set(prev).add(priceId)); try { const currentPrices = prices === 'include-by-default' ? {} : prices; const { [priceId]: _, ...remainingPrices } = currentPrices as Record<string, Price>; await project.updateConfig({ [`payments.products.${productId}.prices`]: Object.keys(remainingPrices).length > 0 ? remainingPrices : {}, }); toast({ title: "Price deleted" }); } finally { setDeletingPriceIds(prev => { const next = new Set(prev); next.delete(priceId); return next; }); } - }; + });
937-945: Use 'never' sentinel for non-repeating items.Line 939 sets
repeat: 'once'for non-repeating items, butshortIntervalLabelin utils expects'never'as the sentinel for non-repeating. This inconsistency will cause incorrect interval rendering.Change the sentinel value to match the expected convention:
- const repeat: DayInterval | 'once' = editing.repeatSelection === 'once' - ? 'once' + const repeat: DayInterval | 'never' = editing.repeatSelection === 'once' + ? 'never' : [editing.repeatCount, editing.repeatUnit || 'month'];apps/dashboard/src/components/data-table/payment-product-table.tsx (1)
107-107: Hook called inside event handler - violates Rules of Hooks.
project.useConfig()is called inside an asynconClickhandler at line 107. Hooks must be called unconditionally at the top level of a component.Move the hook call to the component level:
function ActionsCell({ product }: { product: PaymentProduct }) { const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const stackAdminApp = useAdminApp(); const project = stackAdminApp.useProject(); + const config = project.useConfig(); return ( <> ... <ActionDialog ... okButton={{ label: "Delete", onClick: async () => { - const config = project.useConfig(); const updatedProducts = typedFromEntries( typedEntries(config.payments.products) .filter(([productId]) => productId !== product.id) ); await project.updateConfig({ "payments.products": updatedProducts }); toast({ title: "Product deleted" }); }, }} /> </> ); }apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (2)
449-449: Hook called inside event handler - violates Rules of Hooks.
project.useConfig()at line 449 is called inside an asynconClickhandler. This violates React's Rules of Hooks and will cause runtime errors.Move the hook call to the component level:
function ProductsList({ ... }: ProductsListProps) { const stackAdminApp = useAdminApp(); const project = stackAdminApp.useProject(); + const config = project.useConfig(); const projectId = useProjectId(); const router = useRouter(); ... okButton={{ label: "Delete", onClick: async () => { if (!productToDelete) return; - const config = project.useConfig(); const updatedProducts = typedFromEntries( typedEntries(config.payments.products) .filter(([productId]) => productId !== productToDelete.id) ); await project.updateConfig({ "payments.products": updatedProducts }); toast({ title: "Product deleted" }); setProductToDelete(null); } }}
447-457: Add error handling to async delete handlers.Both the product deletion handler (lines 447-457) and item deletion handler (lines 584-589) lack error handling. If
project.updateConfigfails, the error will be unhandled.Based on learnings, use
runAsynchronouslyWithAlert:+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";Then wrap the product deletion handler:
okButton={{ label: "Delete", - onClick: async () => { + onClick: () => runAsynchronouslyWithAlert(async () => { if (!productToDelete) return; const config = project.useConfig(); const updatedProducts = typedFromEntries( typedEntries(config.payments.products) .filter(([productId]) => productId !== productToDelete.id) ); await project.updateConfig({ "payments.products": updatedProducts }); toast({ title: "Product deleted" }); setProductToDelete(null); - } + }) }}Apply the same pattern to the item deletion handler at lines 584-589.
🧹 Nitpick comments (2)
apps/dashboard/src/components/editable-grid.tsx (1)
400-402: Consider requiring a stable key for grid items.Using
indexas the key can cause React reconciliation issues if items are reordered, filtered, or removed from the middle of the array. SinceEditableGridis a generic component, consumers might perform these operations, leading to state bugs or incorrect focus behavior.Consider one of these solutions:
Option 1: Add an optional key field to items
type BaseItemProps = { + key?: string, icon: React.ReactNode, name: string, tooltip?: string, }; ... {items.map((item, index) => ( - <GridItemContent key={index} item={item} /> + <GridItemContent key={item.key ?? index} item={item} /> ))}Option 2: Use a combination of name + type as key
-{items.map((item, index) => ( - <GridItemContent key={index} item={item} /> +{items.map((item) => ( + <GridItemContent key={`${item.type}-${item.name}`} item={item} /> ))}If items are guaranteed to be stable and append-only in all usage contexts, the current implementation is acceptable.
packages/stack-ui/src/components/ui/dropdown-menu.tsx (1)
145-149: Consider constraining icon size and adding accessibility attributes.The icon container has
h-4 w-4, but the icon content itself isn't constrained. If consumers pass an unsized SVG or component, it could overflow. Additionally, the icon lacks accessibility attributes.Consider applying size constraints to the icon content and adding accessibility:
{icon && ( - <span className="absolute left-2.5 flex h-4 w-4 items-center justify-center"> + <span className="absolute left-2.5 flex h-4 w-4 items-center justify-center [&>*]:h-4 [&>*]:w-4" aria-hidden="true"> {icon} </span> )}The
[&>*]selector ensures direct children are sized correctly, andaria-hidden="true"marks decorative icons appropriately.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx(2 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx(18 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsx(1 hunks)apps/dashboard/src/components/data-table/payment-product-table.tsx(3 hunks)apps/dashboard/src/components/editable-grid.tsx(1 hunks)packages/stack-ui/src/components/ui/dropdown-menu.tsx(10 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Always add new E2E tests when changing the API or SDK interface
For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Use ES6 maps instead of records wherever you can
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxpackages/stack-ui/src/components/ui/dropdown-menu.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,css}: Keep hover/click transitions snappy and fast; avoid fade-in delays on hover. Apply transitions after action completion instead, like smooth fade-out when hover ends
Use hover-exit transitions instead of hover-enter transitions; for example, use 'transition-colors hover:transition-none' instead of fade-in on hover
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxpackages/stack-ui/src/components/ui/dropdown-menu.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
apps/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
{.env*,**/*.{ts,tsx,js}}
📄 CodeRabbit inference engine (AGENTS.md)
Prefix environment variables with STACK_ (or NEXT_PUBLIC_STACK_ if public) so changes are picked up by Turborepo and improves readability
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxpackages/stack-ui/src/components/ui/dropdown-menu.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
apps/dashboard/**/*
📄 CodeRabbit inference engine (AGENTS.md)
When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
apps/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/data-table/payment-product-table.tsx
🧠 Learnings (6)
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Applied to files:
apps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.
Applied to files:
apps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/dashboard/**/* : When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/**/*.{ts,tsx} : NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/**/*.tsx : Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Applied to files:
apps/dashboard/src/components/data-table/payment-product-table.tsx
🧬 Code graph analysis (5)
apps/dashboard/src/components/editable-grid.tsx (3)
packages/stack-ui/src/components/ui/select.tsx (5)
Select(160-160)SelectTrigger(160-160)SelectValue(160-160)SelectContent(160-160)SelectItem(160-160)packages/stack-shared/src/utils/promises.tsx (1)
runAsynchronously(343-366)apps/dashboard/src/components/editable-input.tsx (1)
EditableInput(20-145)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx (1)
packages/stack-shared/src/utils/promises.tsx (1)
runAsynchronously(343-366)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (6)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useProjectId(46-53)apps/dashboard/src/components/router.tsx (1)
useRouter(15-33)packages/stack-shared/src/utils/objects.tsx (2)
typedFromEntries(281-283)typedEntries(263-265)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx (1)
ListSection(19-104)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
PageClient(66-87)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/customers/page-client.tsx (1)
PageClient(51-188)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (6)
Product(8-8)DEFAULT_INTERVAL_UNITS(16-16)Price(9-9)generateUniqueId(155-157)intervalLabel(26-34)shortIntervalLabel(39-45)packages/stack-shared/src/utils/dates.tsx (2)
DayInterval(146-146)fromNow(28-30)apps/e2e/tests/js/convex.test.ts (1)
open(31-33)
apps/dashboard/src/components/data-table/payment-product-table.tsx (1)
packages/stack-shared/src/utils/objects.tsx (2)
typedFromEntries(281-283)typedEntries(263-265)
🪛 Biome (2.1.2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
[error] 449-449: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
apps/dashboard/src/components/data-table/payment-product-table.tsx
[error] 107-107: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: Vercel Agent Review
- GitHub Check: Cursor Bugbot
- GitHub Check: build (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: setup-tests-with-custom-base-port
- GitHub Check: docker
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
- GitHub Check: restart-dev-and-test
- GitHub Check: build (22.x)
- GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
- GitHub Check: all-good
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: setup-tests
🔇 Additional comments (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-card-preview.tsx (1)
1-180: LGTM! Well-structured product card preview component.The component is well-implemented with proper type safety, clean separation of concerns (PriceDisplay subcomponent), and graceful handling of edge cases. The use of shared utilities for formatting and the typedEntries helper ensures consistency across the payments UI.
packages/stack-ui/src/components/ui/dropdown-menu.tsx (4)
85-87: LGTM! Consistent styling refinements.The updated styling (
rounded-xl,border-border/50,p-1.5) creates a more polished appearance with softer borders and refined spacing, consistently applied across bothSubContentandContentcomponents.Also applies to: 103-103
164-164: LGTM! Consistent spacing for indicators.The padding updates (
py-2 pl-9 pr-3) and indicator positioning (left-2.5) align with the new icon implementation inDropdownMenuItem, creating visual consistency across checkbox and radio items.Also applies to: 170-170, 188-188, 193-193
136-140: LGTM! Proper async error handling.The use of
runAsynchronouslyWithAlertcorrectly handles async operations and errors, following the coding guideline to avoid void promises and try-catch-all patterns.
65-66: Padding changes improve accessibility spacing.The updates to
px-3 py-2base padding andpl-9inset padding are minor refinements that align with accessibility best practices. These small adjustments (1-4px differences) enhance spacing for better usability without impacting existing layouts.
| @@ -1,12 +1,13 @@ | |||
| "use client"; | |||
|
|
|||
| import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; | |||
There was a problem hiding this comment.
Critical: Dialog closes before async onCreate completes.
The dialog is closed and form reset immediately after starting the async operation (line 38), not after it completes. If onCreate returns a Promise, the dialog will close before knowing whether the operation succeeded or failed, leaving the user without feedback.
Additionally, based on learnings, runAsynchronouslyWithAlert should be used for form submissions instead of runAsynchronously.
Apply this diff to wait for onCreate to complete before closing:
-import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";- const validateAndCreate = () => {
+ const validateAndCreate = () => {
const newErrors: { id?: string, displayName?: string } = {};
// Validate catalog ID
if (!catalogId.trim()) {
newErrors.id = "Catalog ID is required";
} else if (!/^[a-z0-9-]+$/.test(catalogId)) {
newErrors.id = "Catalog ID must contain only lowercase letters, numbers, and hyphens";
}
// Validate display name
if (!displayName.trim()) {
newErrors.displayName = "Display name is required";
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
- runAsynchronously(onCreate({ id: catalogId.trim(), displayName: displayName.trim() }));
-
- // Reset form
- setCatalogId("");
- setDisplayName("");
- setErrors({});
- onOpenChange(false);
+ runAsynchronouslyWithAlert(async () => {
+ await onCreate({ id: catalogId.trim(), displayName: displayName.trim() });
+
+ // Reset form only after successful creation
+ setCatalogId("");
+ setDisplayName("");
+ setErrors({});
+ onOpenChange(false);
+ });
};Based on learnings, runAsynchronouslyWithAlert handles errors by showing alerts to users, which is appropriate for form submissions.
Also applies to: 38-44
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx
around lines 38 to 44, the dialog is being closed and the form reset immediately
after kicking off the async onCreate call; change this so the component awaits
the async completion and uses runAsynchronouslyWithAlert (instead of
runAsynchronously) to run the submission, awaiting its result before calling
close and reset; specifically, import runAsynchronouslyWithAlert, call await
runAsynchronouslyWithAlert(() => onCreate(values)) (or similar) and only close
the dialog and reset the form after that await completes so errors are handled
and the dialog remains open until the operation finishes.
There was a problem hiding this comment.
Additional Suggestion:
Deleting an item by setting it to null (line 586) causes a runtime crash when sorting items, because the code tries to access properties on the null value.
View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
index 90e18db2..b35a50d4 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
@@ -496,15 +496,17 @@ function ItemsList({
// Sort items by customer type, then by ID
const sortedItems = useMemo(() => {
const customerTypePriority = { user: 1, team: 2, custom: 3 };
- return Object.entries(items).sort(([aId, aItem]: [string, any], [bId, bItem]: [string, any]) => {
- const priorityA = customerTypePriority[aItem.customerType as keyof typeof customerTypePriority] || 4;
- const priorityB = customerTypePriority[bItem.customerType as keyof typeof customerTypePriority] || 4;
- if (priorityA !== priorityB) {
- return priorityA - priorityB;
- }
- // If same customer type, sort by ID
- return stringCompare(aId, bId);
- });
+ return Object.entries(items)
+ .filter(([_, item]) => item !== null) // Skip deleted/null items
+ .sort(([aId, aItem]: [string, any], [bId, bItem]: [string, any]) => {
+ const priorityA = customerTypePriority[aItem.customerType as keyof typeof customerTypePriority] || 4;
+ const priorityB = customerTypePriority[bItem.customerType as keyof typeof customerTypePriority] || 4;
+ if (priorityA !== priorityB) {
+ return priorityA - priorityB;
+ }
+ // If same customer type, sort by ID
+ return stringCompare(aId, bId);
+ });
}, [items]);
// Filter items based on search query
Analysis
Missing null check for items during sorting causes runtime crash on deletion
What fails: When an item is deleted via updateConfig({ [payments.items.{id}]: null }), the sorting logic in ItemsList component attempts to access .customerType on potentially null item values, causing a "Cannot read property 'customerType' of null" error.
How to reproduce:
- In the dashboard at
/projects/[projectId]/payments - In the Items section, click delete on any item
- Confirm deletion - the config updates with
payments.items.{id}: null - During the component re-render, the
sortedItemsuseMemo (line 499) executesObject.entries(items).sort(...) - This crashes when it tries to access
aItem.customerTypeon a null value
Result: TypeError: "Cannot read property 'customerType' of null"
Expected: Items should be filtered out before sorting to prevent accessing properties on null values
Fix applied: Added .filter(([_, item]) => item !== null) before the sort operation to skip deleted/null items, matching the defensive pattern already used for products at line 639 (if (!product) return;). While the config normalization should theoretically delete null keys during updateConfig, defensive client-side filtering prevents potential crashes from race conditions or sync issues.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (10)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (10)
107-107: Accessing catalog without null check will crash if catalog is deleted.If
catalogIdreferences a deleted catalog,config.payments.catalogs[catalogId]returnsundefined, and accessing.displayNamethrows a TypeError. Use optional chaining or explicit null check.- const catalogName = catalogId ? config.payments.catalogs[catalogId].displayName || catalogId : null; + const catalogName = catalogId ? config.payments.catalogs[catalogId]?.displayName || catalogId : null;
152-158: Parent product access lacks null check.If a parent product ID in
isAddOnToreferences a deleted product, accessingconfig.payments.products[parentId].displayNamewill throw. Add optional chaining or filter out missing products.const addOnParents = useMemo(() => { if (product.isAddOnTo === false || typeof product.isAddOnTo !== 'object') return []; - return Object.keys(product.isAddOnTo).map((parentId: string) => ({ + return Object.keys(product.isAddOnTo) + .filter(parentId => config.payments.products[parentId]) + .map((parentId: string) => ({ id: parentId, displayName: config.payments.products[parentId].displayName || parentId, })); }, [product.isAddOnTo, config.payments.products]);
292-294: Free trial state doesn't sync from product when popover opens.Lines 292-294 initialize
freeTrialCountandfreeTrialUnitto hardcoded defaults (7 days). When the popover opens to edit an existing free trial, these values aren't updated fromproduct.freeTrial, so the user sees stale defaults instead of current values.Sync state when popover opens:
+ // Sync free trial state when popover opens + const handleFreeTrialPopoverChange = (open: boolean) => { + if (open && freeTrialInfo) { + setFreeTrialCount(freeTrialInfo[0]); + setFreeTrialUnit(freeTrialInfo[1]); + } + setFreeTrialPopoverOpen(open); + };Then use
onOpenChange={handleFreeTrialPopoverChange}on the Popover at line 472.
332-405: Async handlers lack error handling.All handlers (
handleDisplayNameUpdate,handleCatalogUpdate,handleCreateCatalog,handleStackableUpdate,handleServerOnlyUpdate,handleAddOnSave,handleFreeTrialSave,handleRemoveFreeTrial) callawait project.updateConfig(...)without error handling. Per coding guidelines and learnings, wrap these withrunAsynchronouslyWithAlertfromstackframe/stack-shared/dist/utils/promisesto automatically show alerts on failure.Based on learnings, use
runAsynchronouslyWithAlertfor async button click handlers and form submissions.
374-378: Use alert instead of toast for blocking validation error.Validation errors that block user actions should use alerts (not toasts), as alerts are less easily missed. This blocking validation prevents saving invalid add-on configuration.
Based on learnings, for blocking alerts and errors, never use toast; use alerts instead.
if (isAddOn && selectedAddOnProducts.size === 0) { - toast({ title: "Please select at least one product", variant: "destructive" }); + alert("Please select at least one product"); return; }
670-688: Error handling silently swallows failures inhandleDeletePrice.The try/finally block clears
deletingPriceIdseven if the delete fails, but doesn't catch or surface errors. Failures are invisible to the user.Based on learnings, use
runAsynchronouslyWithAlertfor async handlers instead of manual try/catch blocks.
844-854: Immediate price deletion without confirmation.Clicking the delete button immediately removes the price without confirmation. Accidental clicks can cause unintended data loss.
Consider adding a confirmation dialog or browser
confirm()before callinghandleDeletePrice.
950-953: Item repeat saves 'once' but other code expects 'never'.Line 951 saves
'once'for non-repeating items, but other payments code (e.g.,shortIntervalLabel) uses'never'as the sentinel for non-repeating. This inconsistency can cause incorrect rendering or config state.- const repeat: DayInterval | 'once' = editing.repeatSelection === 'once' - ? 'once' + const repeat: DayInterval | 'never' = editing.repeatSelection === 'once' + ? 'never' : [editing.repeatCount, editing.repeatUnit || 'month'];
1059-1060: Item access lacks null check.If
itemIdreferences a deleted item,config.payments.items[itemId]returnsundefined, and accessing.displayNamewill throw.const itemConfig = config.payments.items[itemId]; + if (!itemConfig) return null; // or handle gracefully const displayName = itemConfig.displayName || itemId;
1334-1334: Hard limit of 100 transactions may hide customers.If a product has more than 100 purchase transactions, some customers won't appear in the list. Implement pagination or at minimum display a warning when the limit is reached.
Consider detecting when
transactions.length === 100and showing a banner like "Showing first 100 customers. [View All]" or implement cursor-based pagination.
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (1)
704-704: Redundant condition check inisFreeNotIncluded.The condition
priceEntries.length === 1is checked twice unnecessarily. Simplify using parentheses for clarity.- const isFreeNotIncluded = priceEntries.length === 1 && priceEntries[0][1].USD === '0' || priceEntries.length === 1 && priceEntries[0][1].USD === '0.00'; + const isFreeNotIncluded = priceEntries.length === 1 && (priceEntries[0][1].USD === '0' || priceEntries[0][1].USD === '0.00');
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Always add new E2E tests when changing the API or SDK interface
For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Use ES6 maps instead of records wherever you can
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,css}: Keep hover/click transitions snappy and fast; avoid fade-in delays on hover. Apply transitions after action completion instead, like smooth fade-out when hover ends
Use hover-exit transitions instead of hover-enter transitions; for example, use 'transition-colors hover:transition-none' instead of fade-in on hover
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
apps/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
{.env*,**/*.{ts,tsx,js}}
📄 CodeRabbit inference engine (AGENTS.md)
Prefix environment variables with STACK_ (or NEXT_PUBLIC_STACK_ if public) so changes are picked up by Turborepo and improves readability
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
apps/dashboard/**/*
📄 CodeRabbit inference engine (AGENTS.md)
When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
apps/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
🧠 Learnings (4)
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/dashboard/**/* : When making changes in the dashboard, provide the user with a deep link to the dashboard page changed, usually in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/... or using a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.
Applied to files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: Vercel Agent Review
- GitHub Check: Cursor Bugbot
- GitHub Check: setup-tests-with-custom-base-port
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: all-good
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
- GitHub Check: restart-dev-and-test
- GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
- GitHub Check: restart-dev-and-test-with-custom-base-port
🔇 Additional comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (1)
1-3: Deep link for testing this product detail page.Based on learnings, when making changes in the dashboard, provide a deep link. To test this product detail page locally, navigate to:
http://localhost:8101/projects/-selector-/payments/products/[productId]Replace
[productId]with an actual product ID from your project configuration.Note: Per PR summary, the new local ports are dashboard 8101, backend 8102.
| setEditingItem(null); | ||
| setIsAddingItem(false); | ||
| setSelectedItemId(''); | ||
| }; |
There was a problem hiding this comment.
Bug: Included items saved with invalid repeat value
ProductItemsSection writes repeat: 'once' into payments.products.*.includedItems, but the config schema uses 'never' (or a DayInterval) for non-repeating items. After saving, code paths like shortIntervalLabel(item.repeat) can break because they don’t handle 'once'.
Additional Locations (1)
| repeatSelection: isOnce ? 'once' : (repeatInterval?.[0] === 1 ? repeatInterval[1] : 'custom') as EditingItem['repeatSelection'], | ||
| repeatCount: repeatInterval?.[0] || 1, | ||
| repeatUnit: repeatInterval?.[1] as DayInterval[1] | undefined, | ||
| }); |
There was a problem hiding this comment.
Bug: Invalid included item repeat value saved
ProductItemsSection writes includedItems[*].repeat as 'once', but the config schema only allows a DayInterval or 'never'. This makes “once” selections produce an invalid config update (likely rejected) and can also cause later code paths to assume a shape that the persisted config can’t represent.
...rc/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
Show resolved
Hide resolved
| setEditingItem(null); | ||
| setIsAddingItem(false); | ||
| setSelectedItemId(''); | ||
| }; |
There was a problem hiding this comment.
Bug: Invalid repeat value stored for items
ProductItemsSection saves includedItems[*].repeat as 'once' when the user selects “Once”, but the config schema for includedItems.repeat only supports a DayInterval or 'never'. This can create invalid config data and can also break rendering logic like shortIntervalLabel(item.repeat) that assumes the 'never' sentinel.
There was a problem hiding this comment.
Bug: Create catalog closes even if creation fails
CreateCatalogDialog now runs onCreate via runAsynchronously(...) and immediately resets state and closes the dialog. If onCreate is async and fails (e.g., updateConfig rejects), the UI still closes and clears inputs, leaving the user with no indication that the catalog wasn’t created.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx#L6-L45
| setEditingItem(null); | ||
| setIsAddingItem(false); | ||
| setSelectedItemId(''); | ||
| }; |
There was a problem hiding this comment.
Bug: Invalid included item repeat value
ProductItemsSection persists included-item repeat as 'once', but the payments config schema (and shortIntervalLabel) expects repeat to be a DayInterval tuple or 'never'. This can make project.updateConfig write invalid config and can break rendering when shortIntervalLabel(item.repeat) receives 'once'.
Summary by CodeRabbit
New Features
Style
Bug Fixes / Chores
✏️ Tip: You can customize this high-level summary in your review settings.
Note
Redesigns Payments with Catalogs and rich Product editing flows, and migrates to a new in-house UI kit replacing stack-ui.
payments/catalogs/*with onboarding/test-mode banners and Connect notifications.payments/products/new, product detailpayments/products/[productId], customers table, and refreshed list view with CSV export and safer deletes.PricingSection,PriceEditDialog,IncludedItemDialog,EditableGrid,EditableInput,RepeatingInput,ProductCardPreviewfor prices/items/free trials and previews.@stackframe/stack-uiwith local@/components/ui(Radix-based) across pages; add many primitives (button,dialog,dropdown-menu,table,toast, etc.).cmdk,react-resizable-panels,react-day-picker,class-variance-authority, etc.).isValidUserSpecifiedId,sanitizeUserSpecifiedId,getUserSpecifiedIdErrorMessage; apply to catalog/price/item/store ID inputs.Written by Cursor Bugbot for commit 6ab66d6. This will update automatically on new commits. Configure here.