-
Notifications
You must be signed in to change notification settings - Fork 484
Payments redesign #1045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Payments redesign #1045
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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. WalkthroughBroad frontend and backend changes around payments/products: new catalogs pages and layouts, UI restyling and input sanitization, revised deletion flows and data utilities, added env flags for email queue control, CI port updates, new e2e tests, and several public-signature tweaks. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas needing extra attention:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
13 files reviewed, 4 comments
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.| onClick: async () => { | ||
| if (!itemToDelete) return; | ||
| await project.updateConfig({ [`payments.items.${itemToDelete.id}`]: null }); | ||
| toast({ title: "Item deleted" }); | ||
| setItemToDelete(null); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Missing error handling - use runAsynchronouslyWithAlert for async onClick handler
| onClick: async () => { | |
| if (!itemToDelete) return; | |
| await project.updateConfig({ [`payments.items.${itemToDelete.id}`]: null }); | |
| toast({ title: "Item deleted" }); | |
| setItemToDelete(null); | |
| } | |
| onClick: () => runAsynchronouslyWithAlert(async () => { | |
| if (!itemToDelete) return; | |
| await project.updateConfig({ [`payments.items.${itemToDelete.id}`]: null }); | |
| toast({ title: "Item deleted" }); | |
| setItemToDelete(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: 580:585
Comment:
**logic:** Missing error handling - use `runAsynchronouslyWithAlert` for async onClick handler
```suggestion
onClick: () => runAsynchronouslyWithAlert(async () => {
if (!itemToDelete) return;
await project.updateConfig({ [`payments.items.${itemToDelete.id}`]: null });
toast({ title: "Item deleted" });
setItemToDelete(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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This 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]); |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[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, ''); |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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(','); |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| onClick: async () => { | ||
| await project.updateConfig({ [`payments.products.${product.id}`]: null }); | ||
| 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" }); | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hook called inside event handler - violates Rules of Hooks.
project.useConfig() at line 108 is called inside an async onClick handler. Hooks must be called unconditionally at the top level of a component, not inside callbacks or nested functions.
Move the config retrieval 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" });
},
}}
/>
</>
);
}Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 Biome (2.1.2)
[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)
🤖 Prompt for AI Agents
In apps/dashboard/src/components/data-table/payment-product-table.tsx around
lines 107 to 115, a React hook project.useConfig() is being called inside an
async onClick handler which violates the Rules of Hooks; move the hook call to
the top level of the component (above return) so it runs unconditionally, then
use the retrieved config inside the onClick handler to compute updatedProducts
and call project.updateConfig; ensure you remove the hook call from the handler
and reference the top-level const config inside the handler.
| <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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
[email protected]semantics: 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
| import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; | ||
| import { Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, toast } from "@stackframe/stack-ui"; | ||
| import { MoreVertical, Plus } from "lucide-react"; | ||
| import { ActionDialog, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, toast } from "@stackframe/stack-ui"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add missing import for error handling utility.
The fixes above require importing runAsynchronouslyWithAlert. Add it to your imports:
-import { ActionDialog, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, toast } from "@stackframe/stack-ui";
+import { ActionDialog, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, toast } from "@stackframe/stack-ui";
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";📝 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.
| import { ActionDialog, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, toast } from "@stackframe/stack-ui"; | |
| import { ActionDialog, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, toast } from "@stackframe/stack-ui"; | |
| 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-list-view.tsx
around line 11, the code uses runAsynchronouslyWithAlert but does not import it;
add runAsynchronouslyWithAlert to the existing import list (import { ...,
runAsynchronouslyWithAlert } from "@stackframe/stack-ui") so the utility is
available where it is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| label: "Delete", | ||
| onClick: async () => { | ||
| if (!productToDelete) return; | ||
| const config = project.useConfig(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: React hook called inside async callback function
project.useConfig() is a React hook being called inside an async onClick callback function. React hooks must only be called at the top level of a function component, not inside callbacks, loops, or conditions. This violates React's Rules of Hooks and will cause a runtime error when the delete button is clicked. The config should be obtained from the component's top-level scope where useConfig() is already called at line 604.
Additional Locations (1)
| run: pnpm run start:backend --log-order=stream & | ||
| wait-on: | | ||
| http://localhost:6702 | ||
| http://localhost:8102 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Custom port test waits on wrong default ports
The custom base port E2E test workflow has NEXT_PUBLIC_STACK_PORT_PREFIX: "67" which configures services to run on ports 6702/6701, but the wait-on URLs were changed from 6702 to 8102 and 8101 (the default ports). This causes the CI to wait indefinitely for services on ports where they're not running, defeating the purpose of testing custom port configuration and causing test failures or timeouts.
Additional Locations (2)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (4)
apps/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts (1)
58-94: LGTM!Test correctly verifies that pending users are filtered out by default when listing users. The test setup with both verified and unverified users provides good coverage.
The
anytype on line 91 could be replaced with a more specific type for better type safety:- const userIds = listResponse.body.items.map((u: any) => u.id); + const userIds = listResponse.body.items.map((u: { id: string }) => u.id);This same pattern appears in lines 129, 161, and 220.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsx (1)
15-66: Consider extracting shared WelcomeScreen component.This
WelcomeScreencomponent is nearly identical to the one inproducts/page-client.tsx(same illustration structure, similar props interface). The only differences are the title, description text, and PageLayout props.You could extract a shared
PaymentsWelcomeScreencomponent that acceptstitle,description, andpageDescriptionprops to reduce duplication.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsx (2)
32-36: Consider error handling for the redirect flow.The
setupPaymentsfunction redirects and waits, but ifstackAdminApp.setupPayments()fails, the error will propagate unhandled. Per coding guidelines, avoid unhandled promise rejections.Consider wrapping in proper error handling or using
runAsynchronouslyWithAlert:const setupPayments = async () => { - const { url } = await stackAdminApp.setupPayments(); - window.location.href = url; - await wait(2000); + try { + const { url } = await stackAdminApp.setupPayments(); + window.location.href = url; + await wait(2000); + } catch (error) { + // Handle error appropriately - show alert to user + throw error; + } };
131-144: Wrap async onClick handler properly.The
onClickhandler calls an async function directly. IfhandleDisableTestModefails, the promise rejection goes unhandled.<Button size="sm" variant="outline" - onClick={() => handleDisableTestMode()} + onClick={async () => { + await handleDisableTestMode(); + }}Or consider using
runAsynchronouslyWithAlertfrom stack-shared utilities if available, to ensure errors surface to the user.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx(4 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page.tsx(1 hunks)apps/dashboard/src/lib/apps-frontend.tsx(1 hunks)apps/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page.tsx
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{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/catalogs/page.tsxapps/dashboard/src/lib/apps-frontend.tsxapps/e2e/tests/backend/endpoints/api/v1/pending-users.test.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/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/catalogs/page.tsxapps/dashboard/src/lib/apps-frontend.tsxapps/e2e/tests/backend/endpoints/api/v1/pending-users.test.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/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/catalogs/page.tsxapps/dashboard/src/lib/apps-frontend.tsxapps/e2e/tests/backend/endpoints/api/v1/pending-users.test.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/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/catalogs/page.tsxapps/dashboard/src/lib/apps-frontend.tsxapps/e2e/tests/backend/endpoints/api/v1/pending-users.test.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/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/catalogs/page.tsxapps/dashboard/src/lib/apps-frontend.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/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/catalogs/page.tsxapps/dashboard/src/lib/apps-frontend.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer .toMatchInlineSnapshot over other selectors in tests when possible; check snapshot-serializer.ts for formatting details
Files:
apps/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts
apps/e2e/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always add new E2E tests when changing API or SDK interface; err on the side of creating too many tests due to the critical nature of the industry
Files:
apps/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts
🧠 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 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/lib/apps-frontend.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/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/e2e/**/*.{ts,tsx} : Always add new E2E tests when changing API or SDK interface; err on the side of creating too many tests due to the critical nature of the industry
Applied to files:
apps/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts
📚 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/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts
🧬 Code graph analysis (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page.tsx (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page.tsx (2)
metadata(3-5)Page(11-15)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsx (1)
PageClient(68-108)
apps/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts (3)
apps/e2e/tests/helpers.ts (1)
it(12-12)apps/backend/src/route-handlers/smart-response.tsx (1)
createResponse(63-138)apps/e2e/tests/backend/backend-helpers.ts (1)
niceBackendFetch(109-173)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsx (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/app-enabled-guard.tsx (1)
AppEnabledGuard(11-37)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(29-44)packages/stack-shared/src/utils/urls.tsx (1)
url(278-280)packages/stack-shared/src/utils/promises.tsx (1)
wait(260-268)apps/dashboard/src/components/form-dialog.tsx (1)
SmartFormDialog(11-51)apps/dashboard/src/components/form-fields.tsx (1)
SelectField(234-271)packages/stack-ui/src/components/action-dialog.tsx (1)
ActionDialog(31-135)
⏰ 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: restart-dev-and-test-with-custom-base-port
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: all-good
- GitHub Check: setup-tests-with-custom-base-port
- GitHub Check: lint_and_build (latest)
- GitHub Check: E2E Tests (Node 22.x, Freestyle mock)
- GitHub Check: docker
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
🔇 Additional comments (13)
apps/e2e/tests/backend/endpoints/api/v1/pending-users.test.ts (7)
1-3: LGTM!Imports are appropriate and follow the project's test structure conventions.
7-32: LGTM!Test correctly verifies that users with unverified emails are marked as pending when
requireEmailVerificationis enabled. The approach of creating users via server API ensures the email remains unverified.
34-56: LGTM!Test correctly verifies that users with verified emails are not marked as pending, leveraging the OTP sign-in flow which verifies the email as part of the authentication process.
96-132: LGTM!Test correctly verifies that setting
include_pending=truein the query parameter includes pending users in the list results.
167-222: LGTM!Both tests correctly verify that when
requireEmailVerificationis disabled, users with unverified emails are not marked as pending and are included in default user lists.
226-245: LGTM!Test correctly verifies that anonymous users are never marked as pending, even when
requireEmailVerificationis enabled. This is the expected behavior since anonymous users don't have emails to verify.
249-274: LGTM!Test correctly verifies that the
requireEmailVerificationconfiguration can be enabled viaProject.updateConfigand that the setting is immediately applied to newly created users.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
63-91: LGTM! Clean simplification of the page structure.The component properly checks for existing products/items before showing the welcome screen, and the
useMemodependencies are correctly specified. The flow from welcome screen to list view is straightforward.As per coding guidelines, here's the deep link to test this page:
http://localhost:8101/projects/-selector-/payments/productsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page.tsx (1)
1-12: LGTM! Standard Next.js page structure.The page follows the established pattern used in other payment pages (e.g.,
products/page.tsx). Clean and minimal.Deep link to test:
http://localhost:8101/projects/-selector-/payments/catalogsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/page-client.tsx (1)
68-108: LGTM! Well-structured client component with proper state management.The draft state pattern (
draftRequestId,draftCustomerType,onDraftHandled) provides a clean way to coordinate between the welcome screen and the catalogs view. TheuseMemodependencies are correctly specified.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsx (2)
85-224: LGTM! Well-structured layout with comprehensive onboarding states.The layout handles multiple states cleanly:
- No Stripe account → setup card
- Test mode active → blue banner
- Incomplete onboarding → amber warning banner
- Connected → notification banner + children
The styling is consistent and follows the transition pattern from coding guidelines (
transition-colors duration-150 hover:transition-none).
261-376: Multi-step dialog flow looks good overall.The country-select → US/Other branching provides a clear user flow. The
resetAndClosehelper ensures state resets properly when dialogs close.One minor observation: the "Start Setup" button is duplicated across all three screen states (lines 289, 307, 345). Consider extracting it as a shared element.
apps/dashboard/src/lib/apps-frontend.tsx (1)
139-140: Navigation items updated correctly for the new Catalogs page.The navigation structure looks good with "Catalogs" as the first item and "Products & Items" second, matching the new page structure.
Based on learnings, when adding new apps or pages, both
apps-frontend.tsxandapps-config.tsshould be updated. Please verify thatapps-config.tshas corresponding changes if needed for the new Catalogs route.#!/bin/bash # Check if apps-config.ts has any payments-related configuration that might need updating rg -n "payments|catalogs|products" --type=ts packages/stack-shared/src/apps/apps-config.ts 2>/dev/null || echo "File not found or no matches"
| const handleDoThisLater = async () => { | ||
| await stackAdminApp.setupPayments(); | ||
| window.location.reload(); | ||
| // Call setup endpoint but don't open URL | ||
| setIsOpen(false); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: window.location.reload() may not execute after setIsOpen(false).
After window.location.reload() is called, the subsequent setIsOpen(false) line is unreachable. Also, the comment says "Call setup endpoint but don't open URL", but setupPayments() in the outer scope redirects via window.location.href = url. This seems inconsistent with the intent.
Did you mean to call stackAdminApp.setupPayments() directly here without redirecting?
const handleDoThisLater = async () => {
await stackAdminApp.setupPayments();
- window.location.reload();
- // Call setup endpoint but don't open URL
setIsOpen(false);
+ window.location.reload();
};Or if the intent is to NOT redirect and just initialize the backend state:
const handleDoThisLater = async () => {
await stackAdminApp.setupPayments();
- window.location.reload();
- // Call setup endpoint but don't open URL
setIsOpen(false);
+ // Optionally reload to refresh state
+ window.location.reload();
};📝 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.
| const handleDoThisLater = async () => { | |
| await stackAdminApp.setupPayments(); | |
| window.location.reload(); | |
| // Call setup endpoint but don't open URL | |
| setIsOpen(false); | |
| }; | |
| const handleDoThisLater = async () => { | |
| await stackAdminApp.setupPayments(); | |
| setIsOpen(false); | |
| window.location.reload(); | |
| }; |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/catalogs/layout.tsx
around lines 249-254, the code calls await stackAdminApp.setupPayments(); then
window.location.reload(); then setIsOpen(false), but the reload makes the
subsequent setIsOpen unreachable and setupPayments currently performs a redirect
via window.location.href which conflicts with the comment intent. Fix by
choosing one behavior: either 1) if you want to initialize backend without
redirecting, change to call a non-redirecting setup method or add a
flag/parameter to stackAdminApp.setupPayments to prevent client-side navigation
and await that before calling setIsOpen(false) and then reload (or remove
reload); or 2) if you want to redirect, remove window.location.reload() and/or
the setIsOpen(false) since navigation will occur; ensure the order is await
setup (non-redirecting) -> setIsOpen(false) -> window.location.reload() if
reload is required.
| it("should include pending users when include_anonymous is true", async ({ expect }) => { | ||
| await Project.createAndSwitch({ | ||
| config: { | ||
| magic_link_enabled: true, | ||
| }, | ||
| }); | ||
| await Project.updateConfig({ | ||
| onboarding: { requireEmailVerification: true }, | ||
| }); | ||
|
|
||
| // Create an unverified user | ||
| const unverifiedUserResponse = await niceBackendFetch("/api/v1/users", { | ||
| accessType: "server", | ||
| method: "POST", | ||
| body: { | ||
| primary_email: "[email protected]", | ||
| primary_email_auth_enabled: true, | ||
| }, | ||
| }); | ||
| const unverifiedUserId = unverifiedUserResponse.body.id; | ||
|
|
||
| // List users with include_anonymous - should include pending users too | ||
| const listResponse = await niceBackendFetch("/api/v1/users?include_anonymous=true", { | ||
| accessType: "server", | ||
| }); | ||
|
|
||
| expect(listResponse.status).toBe(200); | ||
| const userIds = listResponse.body.items.map((u: any) => u.id); | ||
| expect(userIds).toContain(unverifiedUserId); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarify test intent: include_anonymous including pending users.
The test name "should include pending users when include_anonymous is true" may be confusing because the test only creates a pending (unverified) user, not an anonymous user. If the intended behavior is that include_anonymous also includes pending users, consider either:
- Renaming to clarify: "should include pending users when include_anonymous is true (as a side effect)"
- Adding a comment explaining why
include_anonymousincludes pending users - Or creating both an anonymous user and a pending user to verify both are included
| id: parentId, | ||
| displayName: config.payments.products[parentId].displayName || parentId, | ||
| })); | ||
| }, [product.isAddOnTo, config.payments.products]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: isAddOnTo treated as array instead of object
The ProductHeader component incorrectly treats product.isAddOnTo as an array by checking .length and calling .map() directly on it. However, according to the schema and how it's saved in handleAddOnSave, isAddOnTo is either false or an object like { "product-id": true }. Objects don't have a .length property, so product.isAddOnTo?.length will always return undefined, causing isAddOn to always be false and addOnParents to always be empty. The ProductDetailsSection component correctly uses Object.keys() to handle this object type. This bug prevents the header from showing the "Add-on" badge and parent product links.
|
|
||
| function ProductPage({ productId, product, config }: ProductPageProps) { | ||
| const catalogId = product.catalogId; | ||
| const catalogName = catalogId ? config.payments.catalogs[catalogId].displayName || catalogId : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Missing null checks for referenced config entities
Multiple locations access config entity properties without checking if the entity exists. At line 100, config.payments.catalogs[catalogId] is accessed directly. At line 256, config.payments.products[parentId] is accessed. At line 1075, config.payments.items[itemId] is accessed. If any referenced catalog, product, or item was deleted but the reference remains (stale data), these accesses return undefined and the subsequent .displayName property access will throw a TypeError, crashing the product detail page.
Additional Locations (2)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
♻️ Duplicate comments (2)
apps/dashboard/src/components/editable-grid.tsx (1)
181-189: Same missing error handling pattern.Same issue as
EditableBooleanField- the error is silently swallowed. Apply similar fix.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (1)
1103-1112: Missing delete confirmation for items.Same issue as prices - items can be deleted without confirmation. Consider adding a confirmation dialog.
🧹 Nitpick comments (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page.tsx (1)
7-16: Consider keeping this page static by extractingproductIdclient-side.Per coding guidelines, using
await paramsmakes this page dynamic. Consider restructuring to keep the page static by extracting theproductIdin the client component usinguseParams()fromnext/navigation:-export default async function Page({ - params, -}: { - params: Promise<{ productId: string }>, -}) { - const awaitedParams = await params; - return ( - <PageClient productId={awaitedParams.productId} /> - ); -} +export default function Page() { + return <PageClient />; +}Then in
page-client.tsx, useuseParams()to get theproductId:import { useParams } from "next/navigation"; export default function PageClient() { const { productId } = useParams<{ productId: string }>(); // ... }As per coding guidelines, prefer client components and hooks to keep pages static.
apps/dashboard/src/components/editable-grid.tsx (2)
50-58: Unused props inCustomDropdownItemtype.The
open,onOpenChange, andpopoverContentprops are defined inCustomDropdownItembut never used inCustomDropdownFieldorGridItemValue. Either implement support for these props or remove them to avoid confusion.// Custom dropdown (like Free Trial) - you provide trigger text and popover content type CustomDropdownItem = BaseItemProps & { type: 'custom-dropdown', triggerContent: React.ReactNode, - popoverContent: React.ReactNode, - open?: boolean, - onOpenChange?: (open: boolean) => void, disabled?: boolean, };
348-350: Consider using a stable key instead of array index.Using
indexaskeycan cause rendering issues if items are reordered or filtered. Consider adding an optionalkeyproperty toEditableGridItemor deriving a key fromitem.name:- {items.map((item, index) => ( - <GridItemContent key={index} item={item} /> + {items.map((item) => ( + <GridItemContent key={item.name} item={item} /> ))}apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (1)
1461-1473: Use hover-exit transition pattern.Per coding guidelines, use hover-exit transitions instead of hover-enter transitions. The current
transition-colorsapplies on both enter and exit:- <div className="flex items-center gap-2 hover:text-primary transition-colors"> + <div className="flex items-center gap-2 hover:text-primary transition-colors hover:transition-none">Apply the same fix to
TeamCellat line 1491.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
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/[productId]/page.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx(2 hunks)apps/dashboard/src/components/editable-grid.tsx(1 hunks)apps/dashboard/src/components/editable-input.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.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/[productId]/page.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/editable-input.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.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/editable-input.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.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/editable-input.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.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/editable-input.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.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/editable-input.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.tsxapps/dashboard/src/components/editable-grid.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsxapps/dashboard/src/components/editable-input.tsx
🧠 Learnings (1)
📚 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/[productId]/page.tsx
🧬 Code graph analysis (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page.tsx (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (1)
PageClient(67-90)
apps/dashboard/src/components/editable-grid.tsx (2)
packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)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 (9)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (7)
Product(8-8)Price(9-9)DEFAULT_INTERVAL_UNITS(16-16)generateUniqueId(139-141)intervalLabel(26-34)PRICE_INTERVAL_UNITS(17-17)shortIntervalLabel(39-45)packages/stack-shared/src/utils/dates.tsx (2)
DayInterval(146-146)fromNow(28-30)packages/stack-shared/src/utils/objects.tsx (1)
typedEntries(263-265)apps/dashboard/src/components/editable-grid.tsx (2)
EditableGridItem(74-80)EditableGrid(337-353)docs/src/components/icons.tsx (2)
Trash2(322-328)Copy(185-205)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (1)
IntervalPopover(71-227)apps/dashboard/src/components/settings.tsx (1)
SettingCard(12-49)packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionEntry(97-97)Transaction(125-125)packages/stack-ui/src/components/data-table/cells.tsx (1)
AvatarCell(45-52)
⏰ 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: E2E Tests (Node 22.x, Freestyle mock)
- GitHub Check: E2E Tests (Node 22.x, Freestyle prod)
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: lint_and_build (latest)
- GitHub Check: setup-tests-with-custom-base-port
- GitHub Check: all-good
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: build (22.x)
- GitHub Check: docker
🔇 Additional comments (4)
apps/dashboard/src/components/editable-input.tsx (1)
44-44: LGTM! Styling improvements enhance readability.The addition of
w-fullensures the component stretches to fill its container, andtext-muted-foregroundprovides clear visual distinction for read-only inputs. Both changes align well with the broader UI refresh in this PR.Also applies to: 82-82
apps/dashboard/src/components/editable-grid.tsx (1)
280-335: LGTM!The discriminated union pattern with exhaustive switch cases is clean and type-safe.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx (2)
67-90: LGTM!Clean conditional rendering with good UX for the "not found" state, including a helpful back button.
1-10: Deep link for testing.As per coding guidelines, here's the deep link to test this page:
http://localhost:8101/projects/-selector-/payments/products/{productId}Replace
{productId}with an actual product ID from your test data.
| const [isAddOn, setIsAddOn] = useState(() => !!product.isAddOnTo && product.isAddOnTo !== false); | ||
| const [selectedAddOnProducts, setSelectedAddOnProducts] = useState<Set<string>>(() => { | ||
| if (!product.isAddOnTo || product.isAddOnTo === false) return new Set(); | ||
| return new Set(Object.keys(product.isAddOnTo)); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for isAddOnTo type definition and all usage patterns
rg -n "isAddOnTo" --type ts -C 3 apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/\[productId\]/page-client.tsxRepository: stack-auth/stack-auth
Length of output: 2536
🏁 Script executed:
#!/bin/bash
# Also search for product type definitions and isAddOnTo declarations
rg -n "isAddOnTo" -t ts | grep -E "(type|interface|\.isAddOnTo)" | head -50Repository: stack-auth/stack-auth
Length of output: 8709
🏁 Script executed:
#!/bin/bash
# Search for the Product type definition and isAddOnTo field
rg -n "type.*Product|interface.*Product" -A 20 packages/stack-shared/src/config/schema.ts | grep -A 20 "isAddOnTo"Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look at the type definition more directly
rg -n "isAddOnTo.*:" packages/stack-shared/src/config/schema.ts -B 2 -A 2Repository: stack-auth/stack-auth
Length of output: 290
🏁 Script executed:
#!/bin/bash
# Find Product type definition in the schema
rg -n "type Product|interface Product" packages/stack-shared/src/config/schema.ts -A 40 | head -100Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/antml:
# Check for product type definition with isAddOnTo field
ast-grep --pattern 'type Product = $_'Repository: stack-auth/stack-auth
Length of output: 1322
🏁 Script executed:
#!/bin/bash
# Find CompleteConfig type definition
rg -n "type CompleteConfig|interface CompleteConfig" packages/stack-shared/src/config/schema.ts -A 5Repository: stack-auth/stack-auth
Length of output: 450
🏁 Script executed:
#!/bin/bash
# Search for the actual isAddOnTo type in the schema
rg -n "isAddOnTo" packages/stack-shared/src/config/schema.ts -B 5 -A 2Repository: stack-auth/stack-auth
Length of output: 1175
🏁 Script executed:
#!/bin/bash
# Look at the full product type definition including isAddOnTo
rg -n "OrderedPaymentsConfig|payments:" packages/stack-shared/src/config/schema.ts -B 2 -A 50 | grep -A 50 "payments:"Repository: stack-auth/stack-auth
Length of output: 7250
🏁 Script executed:
#!/bin/bash
# Look for the complete product schema definition
rg -n "const productSchema|productSchema =" packages/stack-shared/src/config/schema.ts -A 30Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Get the full context of the branchPaymentsSchema to understand isAddOnTo type
rg -n "branchPaymentsSchema" packages/stack-shared/src/config/schema.ts -A 80 | head -150Repository: stack-auth/stack-auth
Length of output: 5421
🏁 Script executed:
#!/bin/bash
# Find productSchema definition
rg -n "const productSchema|export.*productSchema" packages/stack-shared/src/config/schema.ts -A 40Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for productSchema definition more broadly
rg -n "productSchema" packages/stack-shared/src/config/schema.ts | head -20Repository: stack-auth/stack-auth
Length of output: 268
🏁 Script executed:
#!/bin/bash
# Find productSchema in schema-fields
rg -n "productSchema" packages/stack-shared/src/config/schema-fields.ts -B 5 -A 40 | head -100Repository: stack-auth/stack-auth
Length of output: 154
🏁 Script executed:
#!/bin/bash
# Find where schema-fields is located
fd schema-fields packages/stack-sharedRepository: stack-auth/stack-auth
Length of output: 107
🏁 Script executed:
#!/bin/bash
# Find productSchema in the correct file
rg -n "productSchema" packages/stack-shared/src/schema-fields.ts -B 5 -A 40Repository: stack-auth/stack-auth
Length of output: 4264
🏁 Script executed:
#!/bin/bash
# Check if there are type definitions or comments explaining the type change
rg -n "ProductHeader|ProductDetailsSection" apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/\[productId\]/page-client.tsx -B 5 | head -50Repository: stack-auth/stack-auth
Length of output: 1034
🏁 Script executed:
#!/bin/bash
# Look more closely at ProductHeader implementation around lines 130-139
sed -n '123,150p' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/\[productId\]/page-client.tsxRepository: stack-auth/stack-auth
Length of output: 1106
ProductHeader incorrectly treats isAddOnTo as an array, causing runtime failures.
The type of product.isAddOnTo is Record<string, true> | false | undefined (from schema-fields.ts), not an array. Lines 130-139 in ProductHeader use .length and .map() which will fail when isAddOnTo is an object. ProductDetailsSection correctly uses Object.keys() to handle the record type. Replace ProductHeader's implementation to match ProductDetailsSection's approach:
const isAddOn = !!product.isAddOnTo && product.isAddOnTo !== false;
const addOnParents = useMemo(() => {
if (!product.isAddOnTo || product.isAddOnTo === false) return [];
return Object.keys(product.isAddOnTo).map((parentId: string) => ({
id: parentId,
displayName: config.payments.products[parentId].displayName || parentId,
}));
}, [product.isAddOnTo, config.payments.products]);
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
around lines 241 to 245, ProductHeader is treating product.isAddOnTo as an array
(using .length/.map) but the type is Record<string, true> | false | undefined;
replace the flawed logic with the same pattern used in ProductDetailsSection:
compute isAddOn as !!product.isAddOnTo && product.isAddOnTo !== false, and
create an addOnParents array (useMemo) that returns [] when isAddOnTo is
falsy/false, otherwise maps Object.keys(product.isAddOnTo) to parent objects
containing id and displayName resolved from config.payments.products[parentId]
(fallback to parentId); update dependencies to [product.isAddOnTo,
config.payments.products].
| // Free trial state | ||
| const [freeTrialCount, setFreeTrialCount] = useState(7); | ||
| const [freeTrialUnit, setFreeTrialUnit] = useState<DayInterval[1]>('day'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Free trial popover doesn't initialize from existing value.
When opening the free trial popover, it always shows defaults (7 days) instead of the current free trial value. Consider syncing state when popover opens:
+ // Sync free trial state when popover opens
+ const handleFreeTrialPopoverOpen = (open: boolean) => {
+ if (open && freeTrialInfo) {
+ setFreeTrialCount(freeTrialInfo[0]);
+ setFreeTrialUnit(freeTrialInfo[1]);
+ }
+ setFreeTrialPopoverOpen(open);
+ };Then use handleFreeTrialPopoverOpen in the Popover's onOpenChange.
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/[productId]/page-client.tsx
around lines 247-249, the free trial popover always shows the hardcoded defaults
because the local state (freeTrialCount/freeTrialUnit) is never synced from the
product's current free trial value when the popover opens; add a handler (e.g.,
handleFreeTrialPopoverOpen) that reads the existing free trial values from the
source of truth (product prop, form state, or whatever variable holds the
current freeTrial count/unit), calls setFreeTrialCount and setFreeTrialUnit to
copy those values into local state when opening, and pass that handler to the
Popover component via onOpenChange so the popover initializes with the actual
current values instead of the defaults.
| const handleAddOnSave = async () => { | ||
| if (isAddOn && selectedAddOnProducts.size === 0) { | ||
| toast({ title: "Please select at least one product", variant: "destructive" }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use alert instead of toast for blocking errors.
Per coding guidelines, blocking alerts and errors should use alerts instead of toasts, as they are less easily missed. This validation error should use an alert:
if (isAddOn && selectedAddOnProducts.size === 0) {
- toast({ title: "Please select at least one product", variant: "destructive" });
+ alert("Please select at least one product");
return;
}Or use your project's alert component/utility for a more polished experience.
📝 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.
| const handleAddOnSave = async () => { | |
| if (isAddOn && selectedAddOnProducts.size === 0) { | |
| toast({ title: "Please select at least one product", variant: "destructive" }); | |
| return; | |
| } | |
| const handleAddOnSave = async () => { | |
| if (isAddOn && selectedAddOnProducts.size === 0) { | |
| alert("Please select at least one product"); | |
| return; | |
| } |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
around lines 323 to 327, the validation currently shows a toast for a blocking
error when no add-on products are selected; replace this toast with the
project's blocking alert component or alert utility so the message cannot be
missed. Update the branch where isAddOn && selectedAddOnProducts.size === 0 to
call the alert API (or render the Alert component) with the same message and
destructive/severity styling, then return as before; ensure any imported alert
helper/component is added to the file imports.
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| className="h-5 w-5 p-0 text-destructive hover:text-destructive" | ||
| onClick={() => handleDeletePrice(priceId)} | ||
| > | ||
| <Trash2 className="h-3 w-3" /> | ||
| </Button> | ||
| </SimpleTooltip> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing delete confirmation for prices.
The PR description mentions "delete confirmations", but clicking the delete button immediately deletes the price without confirmation. Consider adding a confirmation dialog to prevent accidental deletions:
- onClick={() => handleDeletePrice(priceId)}
+ onClick={() => {
+ if (confirm(`Delete this price ($${price.USD} ${intervalText})?`)) {
+ handleDeletePrice(priceId);
+ }
+ }}Or use a proper confirmation dialog component for a more polished UX.
📝 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 | |
| variant="ghost" | |
| size="sm" | |
| className="h-5 w-5 p-0 text-destructive hover:text-destructive" | |
| onClick={() => handleDeletePrice(priceId)} | |
| > | |
| <Trash2 className="h-3 w-3" /> | |
| </Button> | |
| </SimpleTooltip> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-5 w-5 p-0 text-destructive hover:text-destructive" | |
| onClick={() => { | |
| if (confirm(`Delete this price ($${price.USD} ${intervalText})?`)) { | |
| handleDeletePrice(priceId); | |
| } | |
| }} | |
| > | |
| <Trash2 className="h-3 w-3" /> | |
| </Button> | |
| </SimpleTooltip> |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
around lines 762-770, clicking the delete Button immediately calls
handleDeletePrice without any confirmation; add a confirmation step (either a
browser confirm or the app's Dialog/Modal component) that opens when the delete
button is clicked, passing the priceId to the dialog, and only call
handleDeletePrice(priceId) after the user explicitly confirms; ensure canceling
the dialog does nothing, the tooltip remains functional, and the dialog is
keyboard-accessible and properly focused/closed after action.
| function ProductCustomersSection({ productId, product }: ProductCustomersSectionProps) { | ||
| const adminApp = useAdminApp(); | ||
| // Get transactions filtered by this product | ||
| const { transactions } = adminApp.useTransactions({ limit: 100 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard limit of 100 transactions may hide customers.
If a product has more than 100 purchase transactions, some customers may not appear in the list. Consider adding pagination or at minimum displaying a warning when the limit is reached:
- const { transactions } = adminApp.useTransactions({ limit: 100 });
+ const { transactions, hasMore } = adminApp.useTransactions({ limit: 100 });
// ...
// In the render, show a warning if hasMore is trueCommittable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
around line 1314, the call adminApp.useTransactions({ limit: 100 }) imposes a
hard cap that can hide customers; replace this with a paginated approach (or at
minimum detect when the returned count equals the limit) by updating the hook
call to accept pagination params (cursor or page/offset and pageSize) and
surface hasMore/nextCursor from the hook, then implement UI controls to fetch
additional pages (Next/Prev or infinite scroll) or display a clear
warning/banner when the returned transactions length === limit indicating more
results exist; ensure the component state tracks current page/cursor and updates
the hook parameters when navigating so all transactions/customers can be
retrieved.
| const handleChange = async (newValue: string) => { | ||
| if (!onUpdate) return; | ||
| setIsUpdating(true); | ||
| try { | ||
| await onUpdate(newValue === 'true'); | ||
| } finally { | ||
| setIsUpdating(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling in handleChange.
If onUpdate throws, the error is silently swallowed by the try/finally without a catch. Per coding guidelines, errors should be properly handled or propagated. Consider using runAsynchronouslyWithAlert or at minimum re-throwing the error after cleanup:
const handleChange = async (newValue: string) => {
if (!onUpdate) return;
setIsUpdating(true);
try {
await onUpdate(newValue === 'true');
+ } catch (error) {
+ throw error;
} finally {
setIsUpdating(false);
}
};Alternatively, integrate with your error handling utilities like runAsynchronouslyWithAlert.
📝 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.
| const handleChange = async (newValue: string) => { | |
| if (!onUpdate) return; | |
| setIsUpdating(true); | |
| try { | |
| await onUpdate(newValue === 'true'); | |
| } finally { | |
| setIsUpdating(false); | |
| } | |
| }; | |
| const handleChange = async (newValue: string) => { | |
| if (!onUpdate) return; | |
| setIsUpdating(true); | |
| try { | |
| await onUpdate(newValue === 'true'); | |
| } catch (error) { | |
| throw error; | |
| } finally { | |
| setIsUpdating(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In apps/dashboard/src/components/editable-grid.tsx around lines 124 to 132, the
handleChange function currently uses try/finally which clears the updating flag
but swallows any error from onUpdate; change the implementation to catch errors
and either re-throw after cleanup or use the project helper
runAsynchronouslyWithAlert to surface failures. Concretely: wrap the await
onUpdate call in try/catch/finally, call setIsUpdating(false) in finally, and in
catch either call runAsynchronouslyWithAlert to report the error or re-throw the
caught error so it isn’t silently swallowed.
Summary by CodeRabbit
New Features
Style
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.
Note
Overhauls the Payments UI (new Catalogs, Products & Items, product detail, editable components), adds email queue branching env controls, and updates workflows/tests and shared UI utilities.
catalogspages and navigation; pricing-table style product grouping by catalog and customer type.EditableGrid/EditableInputcomponents.STACK_EMAIL_BRANCHING_DISABLE_QUEUE_AUTO_TRIGGERandSTACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING; usesgetEnvBooleanto gate auto‑trigger/sending.pending-userssuite for email‑verification pending status; tweaks OTP wait timing.http://localhost:8101/8102across jobs.getEnvBoolean; refine dropdown menu spacing/icons; update tables to handle non‑null products and object rebuild deletes.Written by Cursor Bugbot for commit fcbe3f3. This will update automatically on new commits. Configure here.