Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds new payments UI components and utilities (interval selector, price row, ID helpers), refactors catalog and list product views with editing/draft flows, introduces a test-mode banner and per-project payments config handling, extends router and CodeBlock props, and adds a useQueryState hook. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Layout as Payments Layout
participant Config as Project Config
participant Banner as Test Mode Banner
User->>Layout: Open payments products page
Layout->>Config: Read paymentsConfig.testMode
alt payments.testMode = true
Config-->>Layout: testMode true
Layout->>Banner: render test-mode banner
User->>Banner: Click "Disable test mode"
Banner->>Config: update payments.testMode -> false
Config-->>Layout: config updated
Layout->>Layout: re-render onboarding/onboarding prompt
else payments.testMode = false
Config-->>Layout: testMode false
Layout->>Layout: render onboarding or products view
end
sequenceDiagram
participant Editor as Product Editor
participant PriceRow as ProductPriceRow
participant Popover as IntervalPopover
participant Store as Config Update
Editor->>PriceRow: Render price item
Editor->>PriceRow: Click edit
PriceRow->>Popover: open interval selector
User->>Popover: choose preset/custom/one-time
Popover-->>PriceRow: onChange(DayInterval|null)
PriceRow->>PriceRow: update local state
User->>PriceRow: Update amount / free trial
User->>PriceRow: Click save
PriceRow->>Store: onSave(buildPriceUpdate(...))
Store-->>PriceRow: update persisted config
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings, 1 inconclusive)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
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 |
There was a problem hiding this comment.
Pull Request Overview
This PR implements comprehensive updates to the product/payments page, including a new custom hook for URL query state management, enhanced router functionality, and a complete redesign of the product editing interface with improved UI components and better separation of concerns.
Key changes:
- Added
useQueryStatehook for persistent view state in URL parameters - Refactored product card editing interface with expanded form layout and better visual hierarchy
- Extracted shared UI components and utility functions into separate modules for better maintainability
Reviewed Changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/stack-shared/src/utils/react.tsx | Adds useQueryState hook for URL query parameter state management |
| apps/dashboard/src/components/router.tsx | Enhances router methods to accept full parameter spread |
| apps/dashboard/src/components/code-block.tsx | Adds styling props for flexible CodeBlock rendering |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts | New utility file with type definitions and helper functions for product/price operations |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx | New component for price editing with full form controls |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx | Refactors page layout with query state and unified header |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx | Removes duplicate code and integrates shared components |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx | Major refactor with expanded editing UI and hash-based navigation |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx | Adds test mode banner with disable functionality |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx | Adds ID sanitization for catalog creation |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx | New file with shared UI components (IntervalPopover, OrSeparator, SectionHeading) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| useEffect(() => { | ||
| const updateFromHash = () => { | ||
| if (window.location.hash === currentHash) return; | ||
| setCurrentHash(window.location.hash); | ||
| }; | ||
| updateFromHash(); | ||
| const interval = setInterval(() => updateFromHash(), 10); |
There was a problem hiding this comment.
Polling window.location.hash every 10ms is inefficient. Use the 'hashchange' event listener instead: window.addEventListener('hashchange', updateFromHash). This provides the same functionality without continuous polling.
.../app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Greptile Overview
Greptile Summary
This PR refactors the payments products page with improved code organization and adds test mode functionality.
Key Changes:
- Extracted reusable UI components (
IntervalPopover,OrSeparator,SectionHeading) intocomponents.tsx - Created
utils.tsfor shared types, constants, and helper functions - Moved
ProductPriceRowcomponent to its own file for better modularity - Added test mode banner and toggle functionality in the UI
- Improved catalog ID input sanitization in
create-catalog-dialog.tsx - Added
useQueryStatehook tostack-sharedfor URL query parameter management - Enhanced
CodeBlockcomponent with new display options
Issues Found:
- Missing error handling on two async button click handlers in
layout.tsx(lines 38-40 and 140) - The removal of
runAsynchronouslyWithAlertbreaks error handling for the "Continue setup" button - The new
handleDisableTestModefunction also lacks error handling
Confidence Score: 4/5
- Safe to merge with one critical error handling issue that needs fixing
- The refactoring is well-executed with clean code organization, but missing error handling on async operations violates project custom rules and could lead to silent failures that confuse users
- Pay close attention to
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx- needs error handling restored
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx | 5/5 | New file extracting reusable UI components (OrSeparator, SectionHeading, IntervalPopover) - clean refactoring with no issues |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts | 5/5 | New utility file extracting type definitions, constants, and helper functions - good code organization |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx | 4/5 | Added test mode banner and removed runAsynchronouslyWithAlert - missing error handling on button click |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx | 5/5 | Major refactoring extracting components and utilities to separate files - improved code organization |
Sequence Diagram
sequenceDiagram
participant User
participant PageClient
participant PageClientCatalogsView
participant ProductDialog
participant IntervalPopover
participant Config
User->>PageClient: Visit products page
PageClient->>PageClient: Check if products/items exist
alt No products/items
PageClient->>User: Show WelcomeScreen
User->>PageClient: Click "Create First Product"
PageClient->>PageClient: Set welcomeDismissed=true
end
PageClient->>PageClientCatalogsView: Render catalogs view
User->>PageClient: Toggle test mode switch
PageClient->>Config: updateConfig(payments.testMode)
Config-->>PageClient: Config updated
PageClient->>User: Show toast notification
User->>ProductDialog: Create/edit product
ProductDialog->>IntervalPopover: Select billing frequency
IntervalPopover->>IntervalPopover: User selects interval
IntervalPopover->>ProductDialog: Return interval value
ProductDialog->>Config: Save product configuration
Config-->>ProductDialog: Product saved
User->>PageClient: Toggle view (Pricing table ↔ List)
PageClient->>PageClient: Update URL query param
PageClient->>User: Switch view
11 files reviewed, 2 comments
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (1)
549-569: Blocking error uses toast; switch to an alert or inline error.“Price ID already exists” prevents save; per dashboard guideline, blocking errors shouldn’t use toast.
- if (Object.prototype.hasOwnProperty.call(nextPrices, newId)) { - toast({ title: "Price ID already exists" }); + if (Object.prototype.hasOwnProperty.call(nextPrices, newId)) { + alert("Price ID already exists"); return prev; // Do not change state }As per coding guidelines
🧹 Nitpick comments (12)
apps/dashboard/src/components/code-block.tsx (1)
50-50: LGTM with optional readability suggestion.The conditional padding and border logic is correct and handles all prop combinations appropriately.
For improved readability, you could extract the padding logic into a helper function:
const getHeaderPadding = () => { if (props.noSeparator) return "pt-1 pb-0"; return props.compact ? "py-1" : "py-2"; };Then use it as:
-<div className={cn("text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center", props.compact && !props.noSeparator && "py-1", !props.compact && !props.noSeparator && "py-2", props.noSeparator && "pt-1 pb-0", !props.noSeparator && "border-b")}> +<div className={cn("text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center", getHeaderPadding(), !props.noSeparator && "border-b")}>apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx (1)
74-76: Sanitization vs validation mismatch (underscores).You keep underscores in onChange but validation rejects them (regex only allows a–z, 0–9, hyphen). Align both; prefer the shared sanitizer for consistency.
Apply:
-import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, SimpleTooltip, Typography } from "@stackframe/stack-ui"; +import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, SimpleTooltip, Typography } from "@stackframe/stack-ui"; +import { sanitizeId } from "./utils"; @@ - onChange={(e) => { - const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-'); - setCatalogId(value); - setErrors(prev => ({ ...prev, id: undefined })); - }} + onChange={(e) => { + setCatalogId(sanitizeId(e.target.value)); + setErrors(prev => ({ ...prev, id: undefined })); + }}If underscores are intended, update the validation to
/^[a-z0-9_-]+$/and error text to mention underscores. Based on guidelines.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx (1)
38-40: Add async state and error handling for test mode + onboarding.Disabling test mode and starting onboarding lack error handling and allow double clicks.
- Disable CTA while awaiting, and catch failures with inline messaging (not toast for blocking flows).
- Example:
-const handleDisableTestMode = async () => { - await project.updateConfig({ "payments.testMode": false }); -}; +const [disabling, setDisabling] = useState(false); +const handleDisableTestMode = async () => { + try { + setDisabling(true); + await project.updateConfig({ "payments.testMode": false }); + } catch (e) { + // TODO: surface inline alert/banner here + } finally { + setDisabling(false); + } +}; @@ - <Button + <Button size="lg" variant="outline" - onClick={() => handleDisableTestMode()} + onClick={() => handleDisableTestMode()} + disabled={disabling} >Similarly wrap setupPayments() with try/catch and disable its buttons during the request. As per coding guidelines.
Also applies to: 83-111, 140-141
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (4)
151-163: Avoid saving on every keystroke; debounce amount updates.Current onChange calls savePriceUpdate() on each character, which can thrash parent state or trigger extra work.
Apply a small debounce:
+ const amountSaveTimer = useRef<number | null>(null); @@ - onChange={(e) => { + onChange={(e) => { const v = e.target.value; if (v === '' || /^\d*(?:\.?\d{0,2})?$/.test(v)) setAmount(v); - savePriceUpdate(); + if (amountSaveTimer.current) window.clearTimeout(amountSaveTimer.current); + amountSaveTimer.current = window.setTimeout(() => { + savePriceUpdate(); + }, 250); }}
67-69: Trigger label can be stale during edits.intervalText is derived from price.interval; while editing, the trigger shows the old label until parent updates.
Compute from local state:
-const intervalText = intervalLabel(price.interval); +const intervalText = intervalSelection === 'one-time' + ? null + : intervalLabel([intervalSelection === 'custom' ? intervalCount : 1, priceInterval ?? 'month'] as DayInterval);Also applies to: 175-190
295-301: Consistent currency formatting in view mode.
$${+amount}may render 9.9 instead of 9.90, diverging from saved normalization.Format using the source price or toFixed:
- <div className="text-xl font-semibold tabular-nums"> - {isFree ? 'Free' : `$${niceAmount}`} - </div> + <div className="text-xl font-semibold tabular-nums"> + {isFree ? 'Free' : `$${(+price.USD || 0).toFixed(2).replace(/\.00$/, '')}`} + </div>
20-30: Unused prop: existingPriceIds.Prop is not referenced. If not needed, drop it to reduce API surface.
Would you like me to remove it and update call sites?
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (1)
180-221: Throttle scroll/resize path updates (perf).updatePath runs on every scroll/resize event; add rAF coalescing to avoid jank in large lists.
Apply:
useEffect(() => { if (!fromRef.current || !toRef.current || !containerRef.current) return; - - const updatePath = () => { + let raf = 0; + const updatePathNow = () => { const container = containerRef.current; const from = fromRef.current; const to = toRef.current; @@ setPath(pathStr); setMidpoint({ x: midX, y: midY }); }; - - updatePath(); - window.addEventListener('resize', updatePath); - window.addEventListener('scroll', updatePath, true); + const schedule = () => { + if (raf) cancelAnimationFrame(raf); + raf = requestAnimationFrame(updatePathNow); + }; + schedule(); + window.addEventListener('resize', schedule); + window.addEventListener('scroll', schedule, true); return () => { - window.removeEventListener('resize', updatePath); - window.removeEventListener('scroll', updatePath, true); + if (raf) cancelAnimationFrame(raf); + window.removeEventListener('resize', schedule); + window.removeEventListener('scroll', schedule, true); }; }, [fromRef, toRef, containerRef]);Keeps transitions snappy per animation guidance. As per coding guidelines.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (1)
94-105: Prefer Map over Record for labels (project TS guideline).Use ES6 Map for label lookup and access with .get().
- const buttonLabels: Record<DayInterval[1], string> = useDurationLabels ? { - day: '1 day', - week: '1 week', - month: '1 month', - year: '1 year', - } : { - day: 'daily', - week: 'weekly', - month: 'monthly', - year: 'yearly', - }; + const buttonLabels = new Map<DayInterval[1], string>( + useDurationLabels + ? [['day','1 day'],['week','1 week'],['month','1 month'],['year','1 year']] + : [['day','daily'],['week','weekly'],['month','monthly'],['year','yearly']] + ); @@ - {buttonLabels[unitOption]} + {buttonLabels.get(unitOption)}Also applies to: 173-174
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (2)
39-45: Use Map for unit suffix lookup.Align with project guidance and avoid object key drift.
- const map: Record<DayInterval[1], string> = { day: 'd', week: 'wk', month: 'mo', year: 'yr' }; - const suffix = map[unit]; + const map = new Map<DayInterval[1], string>([['day','d'],['week','wk'],['month','mo'],['year','yr']]); + const suffix = map.get(unit)!;
128-130: Low-entropy IDs; add randomness to reduce collision risk.Two calls in the same ms can collide. Mix time and random.
-export function generateUniqueId(prefix: string): string { - return `${prefix}-${Date.now().toString(36).slice(2, 8)}`; -} +export function generateUniqueId(prefix: string): string { + const t = Date.now().toString(36); + const r = Math.random().toString(36).slice(2, 6); + return `${prefix}-${t.slice(-6)}${r}`; +}apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (1)
719-723: Animation duration is long; prefer snappy transitions.600ms on color transitions feels sluggish in UI. Reduce to ~150–250ms.
- <div className={cn("flex h-full flex-col rounded-3xl border border-border bg-background/95 shadow-lg transition-colors duration-600", + <div className={cn("flex h-full flex-col rounded-3xl border border-border bg-background/95 shadow-lg transition-colors duration-200",As per coding guidelines
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx(4 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx(30 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx(6 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-price-row.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts(1 hunks)apps/dashboard/src/components/code-block.tsx(4 hunks)apps/dashboard/src/components/router.tsx(1 hunks)packages/stack-shared/src/utils/react.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/components/router.tsxpackages/stack-shared/src/utils/react.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/code-block.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/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/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.tsx
apps/{dashboard,dev-launchpad}/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
For blocking alerts and errors in the UI, never use toast; use alerts instead
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/components/router.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/code-block.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/components.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.tsx
apps/{dashboard,dev-launchpad}/**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Animations: keep hover/click transitions snappy; do not delay actions with pre-hover transitions; apply transitions after the action (e.g., fade-out on hover end)
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/components/router.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/code-block.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/components.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.tsx
🧬 Code graph analysis (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (3)
apps/dashboard/src/components/illustrated-info.tsx (1)
IllustratedInfo(4-31)docs/src/components/icons.tsx (1)
Plus(317-320)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
PageClient(72-149)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
packages/stack-shared/src/utils/react.tsx (1)
useQueryState(171-191)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx (1)
packages/stack-shared/src/utils/promises.tsx (1)
wait(260-268)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (2)
packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (1)
DEFAULT_INTERVAL_UNITS(16-16)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (2)
packages/stack-shared/src/config/schema.ts (1)
CompleteConfig(1074-1074)packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (4)
packages/stack-ui/src/components/ui/input.tsx (1)
Input(10-41)apps/dashboard/src/components/link.tsx (1)
Link(21-42)packages/stack-shared/src/utils/react.tsx (1)
useQueryState(171-191)packages/stack-shared/src/utils/arrays.tsx (1)
typedIncludes(3-5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (5)
intervalLabel(26-34)buildPriceUpdate(64-95)PRICE_INTERVAL_UNITS(17-17)DEFAULT_INTERVAL_UNITS(16-16)freeTrialLabel(50-55)packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)packages/stack-ui/src/components/ui/input.tsx (1)
Input(10-41)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (1)
IntervalPopover(77-218)packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)
⏰ 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). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: lint_and_build (latest)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: setup-tests
- GitHub Check: docker
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: build (22.x)
- GitHub Check: Security Check
🔇 Additional comments (5)
apps/dashboard/src/components/router.tsx (1)
20-26: LGTM! Enhanced API flexibility.The updated signatures correctly forward all parameters to the underlying Next.js router methods using spread operators and the
Parametersutility type. This allows callers to pass additional options (e.g.,{ scroll: false }) while preserving the confirmation logic and maintaining backward compatibility.apps/dashboard/src/components/code-block.tsx (4)
4-4: LGTM!The new imports are necessary for the enhanced styling logic and are correctly placed.
Also applies to: 7-7
28-30: LGTM!The new optional props provide clean API surface for controlling visual presentation. The naming is clear and consistent with existing patterns.
49-49: LGTM!The conditional styling logic correctly handles full-width and neutral background variants.
59-59: LGTM!The conditional variant selection ensures proper visual hierarchy and contrast between the button and its background.
| const selectOneTime = () => { | ||
| setIntervalSelection('one-time'); | ||
| setUnit(undefined); | ||
| setCount(1); | ||
| if (!readOnly) onChange(null); | ||
| setOpen(false); | ||
| }; | ||
|
|
There was a problem hiding this comment.
readOnly still mutates parent state; also allow-open in read-only.
Handlers call setIntervalSelection/setUnit/setCount even when readOnly, and Popover can still open. That breaks read-only semantics and can cause unexpected state changes upstream. Also use a semantic button for the trigger.
Apply:
@@
export function IntervalPopover({
readOnly,
@@
}: IntervalPopoverProps) {
- const [open, setOpen] = useState(false);
+ const [open, setOpen] = useState(false);
+ const isInteractive = !readOnly;
@@
- const selectOneTime = () => {
+ const selectOneTime = () => {
+ if (!isInteractive) { setOpen(false); return; }
setIntervalSelection('one-time');
setUnit(undefined);
setCount(1);
if (!readOnly) onChange(null);
setOpen(false);
};
@@
- const selectFixed = (unitOption: DayInterval[1]) => {
+ const selectFixed = (unitOption: DayInterval[1]) => {
+ if (!isInteractive) { setOpen(false); return; }
if (!normalizedUnits.includes(unitOption)) return;
setIntervalSelection(unitOption);
setUnit(unitOption);
setCount(1);
if (!readOnly) onChange([1, unitOption]);
setOpen(false);
};
@@
- const applyCustom = (countValue: number, maybeUnit?: DayInterval[1]) => {
+ const applyCustom = (countValue: number, maybeUnit?: DayInterval[1]) => {
+ if (!isInteractive) return;
const safeUnit = maybeUnit && normalizedUnits.includes(maybeUnit) ? maybeUnit : defaultUnit;
setIntervalSelection('custom');
setUnit(safeUnit);
setCount(countValue);
if (!readOnly) onChange([countValue, safeUnit]);
};
@@
- return (
- <Popover open={open} onOpenChange={setOpen}>
- <PopoverTrigger>
- <div className={cn(triggerClasses, readOnly && "cursor-default")}>
+ return (
+ <Popover open={isInteractive ? open : false} onOpenChange={(v) => isInteractive && setOpen(v)}>
+ <PopoverTrigger asChild>
+ <button
+ type="button"
+ className={cn(triggerClasses, readOnly && "cursor-default")}
+ aria-haspopup="menu"
+ aria-expanded={isInteractive ? open : false}
+ disabled={!isInteractive}
+ >
{triggerLabel}
<ChevronsUpDown className="h-4 w-4" />
- </div>
+ </button>
</PopoverTrigger>Also applies to: 124-131, 133-139, 145-151
.../app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
Outdated
Show resolved
Hide resolved
| const handleSaveEdit = async () => { | ||
| const trimmed = localProductId.trim(); | ||
| const validId = trimmed && /^[a-z0-9-]+$/.test(trimmed) ? trimmed : id; | ||
| if (validId !== id) { | ||
| await onSave(validId, draft); | ||
| await onDelete(id); | ||
| } else { | ||
| await onSave(id, draft); | ||
| } | ||
| setIsEditing(false); | ||
| setEditingPriceId(undefined); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Unify ID validation/sanitization with utils.
Use sanitizeId/isValidId from utils to avoid regex drift and the “_” inconsistency.
@@
- const trimmed = localProductId.trim();
- const validId = trimmed && /^[a-z0-9-]+$/.test(trimmed) ? trimmed : id;
+ const trimmed = localProductId.trim();
+ const validId = trimmed && isValidId(trimmed) ? trimmed : id;
@@
- <Input
+ <Input
className="h-10 rounded-xl border border-border bg-background px-3 text-sm"
value={localProductId}
onChange={(event) => {
- const value = event.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');
- setLocalProductId(value);
+ setLocalProductId(sanitizeId(event.target.value));
}}@@
- const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');
- setNewCatalogId(value);
+ setNewCatalogId(sanitizeId(e.target.value));@@
- if (!/^[a-z0-9-]+$/.test(id)) {
+ if (!isValidId(id)) {
alert("Catalog ID must be lowercase letters, numbers, and hyphens");
return;
}And import:
-import {
+import {
generateUniqueId,
intervalLabel,
shortIntervalLabel,
+ sanitizeId,
+ isValidId,
type Price,
type PricesObject,
type Product
} from "./utils";Also applies to: 748-759, 1189-1191, 1254-1255
|
|
||
| const handleToggleTestMode = async (enabled: boolean) => { | ||
| await project.updateConfig({ "payments.testMode": enabled }); | ||
| toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" }); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
Handle failure when toggling test mode.
If updateConfig fails, the Switch stays toggled and no feedback is shown.
Apply:
-const handleToggleTestMode = async (enabled: boolean) => {
- await project.updateConfig({ "payments.testMode": enabled });
- toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });
-};
+const handleToggleTestMode = async (enabled: boolean) => {
+ try {
+ await project.updateConfig({ "payments.testMode": enabled });
+ toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });
+ } catch (e) {
+ // Optionally surface an inline error and revert
+ await project.updateConfig({ "payments.testMode": !enabled });
+ }
+};Also verify no other places rely on stale paymentsConfig after toggling.
Also applies to: 128-134
🏁 Script executed:
#!/bin/bash
rg -n --hidden -C2 'payments\.testMode' --type=ts --type=tsxLength of output: 27
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for payments.testMode without problematic type filter
rg -n --hidden -C2 'payments\.testMode'
Length of output: 2643
🏁 Script executed:
#!/bin/bash
# Find and examine the specific file
fd 'page-client.tsx' apps/dashboardLength of output: 3415
🏁 Script executed:
#!/bin/bash
# Look for paymentsConfig references in dashboard payments context
rg -n 'paymentsConfig' --hiddenLength of output: 7136
---
🏁 Script executed:
```shell
#!/bin/bash
# View page-client.tsx around lines 100-140 to see both handleToggleTestMode and the second location
cat apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/page-client.tsx | sed -n '95,140p' | cat -n
Length of output: 1785
🏁 Script executed:
#!/bin/bash
# View layout.tsx around line 38-42 to see handleDisableTestMode
cat apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/layout.tsx | sed -n '35,45p' | cat -nLength of output: 361
🏁 Script executed:
#!/bin/bash
# Check if useConfig refetches data or is a static hook
rg -n 'useConfig' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/page-client.tsx -B2 -A2Length of output: 193
Add error handling to test mode toggle and surface errors as alerts per design guidelines.
The Switch component is controlled by paymentsConfig.testMode. If updateConfig fails, the Switch remains toggled while the backend is unchanged, creating UI/backend divergence with no user feedback. Additionally, blocking errors must use alerts, not toast (per design guidelines).
Apply error handling to both locations:
-const handleToggleTestMode = async (enabled: boolean) => {
- await project.updateConfig({ "payments.testMode": enabled });
- toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });
-};
+const handleToggleTestMode = async (enabled: boolean) => {
+ try {
+ await project.updateConfig({ "payments.testMode": enabled });
+ toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });
+ } catch (e) {
+ alert("Failed to update test mode. Please try again.");
+ }
+};Also in layout.tsx line 38–40:
-const handleDisableTestMode = async () => {
- await project.updateConfig({ "payments.testMode": false });
-};
+const handleDisableTestMode = async () => {
+ try {
+ await project.updateConfig({ "payments.testMode": false });
+ } catch (e) {
+ alert("Failed to disable test mode. Please try again.");
+ }
+};📝 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 handleToggleTestMode = async (enabled: boolean) => { | |
| await project.updateConfig({ "payments.testMode": enabled }); | |
| toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" }); | |
| }; | |
| const handleToggleTestMode = async (enabled: boolean) => { | |
| try { | |
| await project.updateConfig({ "payments.testMode": enabled }); | |
| toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" }); | |
| } catch (e) { | |
| alert("Failed to update test mode. Please try again."); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
around lines 105 to 109 and in
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/layout.tsx
around lines 38 to 40, add proper error handling around calls to
project.updateConfig: wrap the async update call in try/catch, revert the local
Switch state if the update fails so UI and backend stay in sync, and surface
failures using the alert mechanism (per design guidelines) instead of toast;
ensure the catch extracts and displays the error message in the alert and that
optimistic state changes are only applied after a successful update (or rolled
back on error).
| const ID_PATTERN = /^[a-z0-9-]+$/; | ||
|
|
||
| /** | ||
| * Validates if an ID matches the required pattern | ||
| */ | ||
| export function isValidId(id: string): boolean { | ||
| return ID_PATTERN.test(id); | ||
| } |
There was a problem hiding this comment.
Inconsistent ID rules: sanitizeId allows “_” but isValidId forbids it.
This will accept underscores in UI then reject on save. Unify to a single policy.
-const ID_PATTERN = /^[a-z0-9-]+$/;
+const ID_PATTERN = /^[a-z0-9-]+$/; // single source of truth
@@
-export function sanitizeId(input: string): string {
- return input.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');
-}
+export function sanitizeId(input: string): string {
+ // keep only chars allowed by ID_PATTERN
+ return input.toLowerCase().replace(/[^a-z0-9-]/g, '-');
+}Also applies to: 135-137
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts
around lines 116-123 (and also update lines 135-137), the ID validation regex
forbids underscores while sanitizeId currently allows them, causing UI
acceptance but server-side rejection; pick one canonical policy and make both
functions consistent: either widen ID_PATTERN to allow underscores (e.g.,
include "_" in the character class) or change sanitizeId to strip/replace
underscores to match the existing pattern, then update any related uses/tests to
reflect the chosen rule so IDs are validated/sanitized consistently across the
codebase.
.../src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
Show resolved
Hide resolved
.../src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx
Show resolved
Hide resolved
...oard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
Show resolved
Hide resolved
.../dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
Show resolved
Hide resolved
.../app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
Outdated
Show resolved
Hide resolved
…/payments/products/page-client-catalogs-view.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Screen.Recording.2025-10-28.at.09.53.23.mov
Summary by CodeRabbit
Release Notes
New Features
Improvements