Skip to content

Product page updates#985

Merged
N2D4 merged 22 commits intodevfrom
product-page-updates
Nov 6, 2025
Merged

Product page updates#985
N2D4 merged 22 commits intodevfrom
product-page-updates

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Oct 28, 2025

Screen.Recording.2025-10-28.at.09.53.23.mov

Summary by CodeRabbit

Release Notes

  • New Features

    • Added test mode banner with toggle in payments configuration
    • New interval selector component for billing frequency options
    • Product and pricing editor with inline editing capabilities
    • Enhanced product list view with search, grouping, and two-column layout
    • Welcome screen for first-time product creation
  • Improvements

    • Automatic ID sanitization for catalog identifiers
    • Query state management for persistent view preferences

Copilot AI review requested due to automatic review settings October 28, 2025 16:42
@vercel
Copy link

vercel bot commented Oct 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 6, 2025 9:28pm
stack-dashboard Ready Ready Preview Comment Nov 6, 2025 9:28pm
stack-demo Ready Ready Preview Comment Nov 6, 2025 9:28pm
stack-docs Ready Ready Preview Comment Nov 6, 2025 9:28pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

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

Walkthrough

Adds 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

Cohort / File(s) Summary
New pricing UI components
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx
Adds OrSeparator, SectionHeading, and exported IntervalPopover (preset/custom/one-time interval selection) and introduces exported ProductPriceRow for inline price editing (amount, interval, free trial, include-by-default).
Pricing utilities module
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts
New exported types (Product, Price, PricesObject), constants (DEFAULT_INTERVAL_UNITS, PRICE_INTERVAL_UNITS), interval formatting helpers, buildPriceUpdate/getPricesObject, and ID utilities (isValidId, generateUniqueId, sanitizeId).
Catalog & list views + Page client
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx, .../page-client-list-view.tsx, .../page-client.tsx, .../layout.tsx
Large refactor: modular editing UI, draft/product/item creation flows, hash-anchor focus styling, two-column desktop / mobile-tab layout, product/item dialogs, test-mode banner (layout) with disable action, and view state moved to useQueryState. Several public prop signatures updated (onCreateNewItem variants, PageClient props).
Catalog ID input normalization
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx
Normalize Catalog ID onChange to lowercase and replace invalid chars with hyphens before storing.
Router and shared utilities
apps/dashboard/src/components/router.tsx, packages/stack-shared/src/utils/react.tsx
useRouter.push/replace now accept variadic args and forward to router; adds exported useQueryState hook to read/write URL query params with history state sync.
CodeBlock enhancements
apps/dashboard/src/components/code-block.tsx
Adds new CodeBlockProps: fullWidth?, neutralBackground?, noSeparator? and adjusts rendering/styling and CopyButton variant accordingly.

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

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Focus review on:
    • page-client-catalogs-view.tsx and page-client-list-view.tsx (large refactors, state/draft flows).
    • IntervalPopover logic (state normalization, onChange/read-only behavior).
    • ProductPriceRow synchronization and save/update paths.
    • Router push/replace signature changes and any call-site compatibility.
    • useQueryState popstate listener lifecycle and history updates.

Possibly related PRs

Poem

🐰
I hopped in the code with a tiny refrain,
Intervals, prices, and IDs made plain.
Drafts take a nibble, banners take flight,
Two columns and tabs in the soft dashboard light.
Eat carrots, ship features — hop, code, delight!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ⚠️ Warning The pull request description consists solely of a URL to an image/video asset with no accompanying textual explanation. While the template provided is minimal (containing only a comment reminding contributors to read CONTRIBUTING.md), the author has not provided any substantive description of the changes, objectives, or rationale for this PR. Given the scope of changes—multiple component rewrites, new UI modules with complex state management, updated public APIs, and structural refactoring across several payment-related files—the absence of any descriptive text makes it impossible for reviewers to understand the intent or context of these modifications.
Title Check ❓ Inconclusive The PR title "Product page updates" is vague and generic, similar to the non-descriptive terms cited in the evaluation criteria. While the title does refer to a real aspect of the changeset (there are indeed updates to the product page), it fails to convey what specifically changed or what the primary objective is. A developer scanning the history cannot determine whether this involves new components, UI reorganization, bug fixes, feature additions, or something else entirely based on this title alone.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ca9dee and 1fe4bc4.

📒 Files selected for processing (2)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (30 hunks)
  • packages/stack-shared/src/utils/react.tsx (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This 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 useQueryState hook 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.

Comment on lines 456 to 462
useEffect(() => {
const updateFromHash = () => {
if (window.location.hash === currentHash) return;
setCurrentHash(window.location.hash);
};
updateFromHash();
const interval = setInterval(() => updateFromHash(), 10);
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

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) into components.tsx
  • Created utils.ts for shared types, constants, and helper functions
  • Moved ProductPriceRow component 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 useQueryState hook to stack-shared for URL query parameter management
  • Enhanced CodeBlock component 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 runAsynchronouslyWithAlert breaks error handling for the "Continue setup" button
  • The new handleDisableTestMode function 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
Loading

11 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0a8e3ff and 4ca9dee.

📒 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.tsx
  • apps/dashboard/src/components/router.tsx
  • packages/stack-shared/src/utils/react.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
  • apps/dashboard/src/components/code-block.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
  • apps/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.tsx
  • apps/dashboard/src/components/router.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
  • apps/dashboard/src/components/code-block.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
  • apps/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.tsx
  • apps/dashboard/src/components/router.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
  • apps/dashboard/src/components/code-block.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx
  • apps/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 Parameters utility 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.

Comment on lines +116 to +123
const selectOneTime = () => {
setIntervalSelection('one-time');
setUnit(undefined);
setCount(1);
if (!readOnly) onChange(null);
setOpen(false);
};

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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

Comment on lines +650 to +661
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);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ 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

Comment on lines +105 to +109

const handleToggleTestMode = async (enabled: boolean) => {
await project.updateConfig({ "payments.testMode": enabled });
toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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=tsx

Length 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/dashboard

Length of output: 3415


🏁 Script executed:

#!/bin/bash
# Look for paymentsConfig references in dashboard payments context
rg -n 'paymentsConfig' --hidden

Length 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 -n

Length 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 -A2

Length 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.

Suggested change
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).

Comment on lines +116 to +123
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

@N2D4 N2D4 requested a review from BilalG1 October 28, 2025 16:57
@N2D4 N2D4 removed their assignment Oct 28, 2025
N2D4 and others added 2 commits November 6, 2025 13:20
…/payments/products/page-client-catalogs-view.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@N2D4 N2D4 merged commit 3cdceb9 into dev Nov 6, 2025
13 of 18 checks passed
@N2D4 N2D4 deleted the product-page-updates branch November 6, 2025 21:22
@coderabbitai coderabbitai bot mentioned this pull request Dec 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants