Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Product page updates#985

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
N2D4 wants to merge19 commits intodev
base:dev
Choose a base branch
Loading
fromproduct-page-updates
Open

Product page updates#985

N2D4 wants to merge19 commits intodevfromproduct-page-updates

Conversation

@N2D4
Copy link
Contributor

@N2D4N2D4 commentedOct 28, 2025
edited
Loading

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

greptile-apps[bot] reacted with thumbs up emoji
CopilotAI review requested due to automatic review settingsOctober 28, 2025 16:42
@vercel
Copy link

vercelbot commentedOct 28, 2025
edited
Loading

The latest updates on your projects. Learn more aboutVercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
stack-backendReadyReadyPreviewCommentNov 5, 2025 11:40am
stack-dashboardReadyReadyPreviewCommentNov 5, 2025 11:40am
stack-demoReadyReadyPreviewCommentNov 5, 2025 11:40am
stack-docsReadyReadyPreviewCommentNov 5, 2025 11:40am

@coderabbitai
Copy link
Contributor

coderabbitaibot commentedOct 28, 2025
edited
Loading

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

This PR adds new UI components and utilities for dashboard payments products, including interval selection and price editing widgets. It refactors product/catalog and list views with editing workflows, adds a test-mode banner to the payments layout, introduces a URL query-state hook, and extends router and CodeBlock component functionality.

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
Introduces OrSeparator, SectionHeading, and IntervalPopover reusable components for interval selection with preset, custom, and one-time options; adds ProductPriceRow component for inline price editing (free and paid prices) with amount, billing frequency, and free trial configuration.
Pricing utilities module
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts
New utility module exporting types (Product, Price, PricesObject), constants (DEFAULT_INTERVAL_UNITS, PRICE_INTERVAL_UNITS), interval formatting helpers (intervalLabel, shortIntervalLabel, freeTrialLabel), price builders (buildPriceUpdate, getPricesObject), and ID utilities (isValidId, generateUniqueId, sanitizeId).
Payments layout and catalog/list views
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.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/page-client-list-view.tsx,apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx
Adds test-mode banner with disable action to layout; refactors catalog/products view with editing workflows, interval selection, and draft management; introduces two-column desktop list layout with mobile tabs, product/item dialogs, and search/grouping; consolidates view state using useQueryState.
Catalog ID input normalization
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx
Sanitizes Catalog ID input to lowercase with invalid characters replaced by hyphens.
Router and shared utilities
apps/dashboard/src/components/router.tsx,packages/stack-shared/src/utils/react.tsx
Extends router push/replace methods to accept variadic arguments; adds useQueryState hook for reading and writing URL query parameters with history state sync.
CodeBlock enhancements
apps/dashboard/src/components/code-block.tsx
Adds fullWidth, neutralBackground, and noSeparator props to CodeBlockProps for conditional styling of container, header, and copy button.

Sequence Diagram(s)

sequenceDiagram    actor User    participant Page as Payments Layout    participant Config as Project Config    participant Banner as Test Mode Banner        User->>Page: Navigate to payments products    Page->>Config: Read paymentsConfig.testMode        alt Test Mode Enabled        Config-->>Page: testMode = true        Page->>Banner: Render test-mode banner        User->>Banner: Click "Disable Test Mode"        Banner->>Config: Update payments.testMode to false        Config-->>Page: Config updated        Page->>Page: Re-render without banner    else Test Mode Disabled        Config-->>Page: testMode = false        Page->>Page: Show onboarding prompt if not submitted    end
Loading
sequenceDiagram    participant Editor as Product Editor    participant PriceRow as ProductPriceRow    participant Popover as IntervalPopover    participant Store as Update Handler        Editor->>PriceRow: Render with price data    PriceRow->>PriceRow: Initialize state from prop        Editor->>PriceRow: Click edit    PriceRow->>PriceRow: Enter edit mode        PriceRow->>Popover: Render interval selector    Editor->>Popover: Select interval preset    Popover->>PriceRow: onChange(DayInterval)    PriceRow->>PriceRow: Update internal state        Editor->>PriceRow: Update amount input    PriceRow->>PriceRow: Update amount state        Editor->>PriceRow: Click save    PriceRow->>Store: onSave(buildPriceUpdate(...))    Store->>Store: Update config
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Focus areas requiring extra attention:
    • page-client-catalogs-view.tsx andpage-client-list-view.tsx: Large-scale refactors with new editing workflows, draft management, and UI composition patterns; verify correctness of state propagation and onChange callbacks across nested components.
    • IntervalPopover component logic: Complex internal state management with preset/custom/one-time modes and unit normalization; ensure edge cases (undefined units, invalid selections) are handled safely.
    • ProductPriceRow synchronization: useEffect that syncs internal state with external prop changes; verify no stale-closure or double-update issues, especially during editing transitions.
    • New public API signatures: Method parameter additions toonCreateNewItem callbacks and removal ofonViewChange from PageClient may impact downstream consumers; trace call sites.
    • Router signature change to variadic arguments: Verify all existing push/replace call sites are compatible and no type breakages occur.
    • useQueryState hook: Ensure popstate listener cleanup and race conditions with URL manipulation are handled correctly.

Possibly related PRs

  • add codeblock tooltips and fix spacing #934: Modifiescode-block.tsx with new props and refactored CodeBlockProps; both PRs extend the same component's public interface.
  • payment test mode fixes #957: Changes payments test-mode UX and control flow (test-mode banner, testMode defaults); overlaps with layout.tsx and test-mode logic in this PR.
  • payment ui fixes #959: Modifieslayout.tsx andpage-client-catalogs-view.tsx with price/OrSeparator and Add Price UI logic; code-level overlap in the same files.

Poem

🐰With intervals selected and prices aligned,
New components sparkle, test modes refined!
Two columns dance, tabs hop and play,
Query states flow through the URL's way!
The dashboard springs forth, fresh and bright!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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⚠️ WarningThe 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❓ InconclusiveThe 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.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branchproduct-page-updates

Thanks for usingCodeRabbit! 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

CopilotAI 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:

  • AddeduseQueryState 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
FileDescription
packages/stack-shared/src/utils/react.tsxAddsuseQueryState hook for URL query parameter state management
apps/dashboard/src/components/router.tsxEnhances router methods to accept full parameter spread
apps/dashboard/src/components/code-block.tsxAdds styling props for flexible CodeBlock rendering
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsNew 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.tsxNew component for price editing with full form controls
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxRefactors page layout with query state and unified header
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxRemoves duplicate code and integrates shared components
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxMajor refactor with expanded editing UI and hash-based navigation
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxAdds test mode banner with disable functionality
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxAdds ID sanitization for catalog creation
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxNew 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 +171 to +180
exportfunctionuseQueryState(key:string,defaultValue?:string){
constgetValue=()=>newURLSearchParams(window.location.search).get(key)??defaultValue??"";

const[value,setValue]=React.useState(getValue);

React.useEffect(()=>{
constonPopState=()=>setValue(getValue());
window.addEventListener("popstate",onPopState);
return()=>window.removeEventListener("popstate",onPopState);
},[]);

Choose a reason for hiding this comment

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

ThegetValue function is defined inside the component but used as a dependency in the effect (line 177). This will cause the effect to recreate the event listener on every render becausegetValue is a new function reference each time. MovegetValue outside the component or wrap it inuseCallback, or inline the logic in theonPopState handler.

Copilot uses AI. Check for mistakes.
Comment on lines +456 to +462
useEffect(()=>{
constupdateFromHash=()=>{
if(window.location.hash===currentHash)return;
setCurrentHash(window.location.hash);
};
updateFromHash();
constinterval=setInterval(()=>updateFromHash(),10);

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.
setDraft(product);
setLocalProductId(id);
}
},[product,id,isEditing]);

Choose a reason for hiding this comment

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

The effect hasisEditing as a dependency, which means whenisEditing changes totrue, the effect runs and updates the draft, potentially overwriting any edits made before entering edit mode. The conditionif (!isEditing) should prevent this, but the dependency should be removed or the logic restructured to avoid this edge case.

Suggested change
},[product,id,isEditing]);
},[product,id]);

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@greptile-appsgreptile-appsbot 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) intocomponents.tsx
  • Createdutils.ts for shared types, constants, and helper functions
  • MovedProductPriceRow component to its own file for better modularity
  • Added test mode banner and toggle functionality in the UI
  • Improved catalog ID input sanitization increate-catalog-dialog.tsx
  • AddeduseQueryState hook tostack-shared for URL query parameter management
  • EnhancedCodeBlock component with new display options

Issues Found:

  • Missing error handling on two async button click handlers inlayout.tsx (lines 38-40 and 140)
  • The removal ofrunAsynchronouslyWithAlert breaks error handling for the "Continue setup" button
  • The newhandleDisableTestMode 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 toapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx - needs error handling restored

Important Files Changed

File Analysis

FilenameScoreOverview
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx5/5New file extracting reusable UI components (OrSeparator, SectionHeading, IntervalPopover) - clean refactoring with no issues
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts5/5New utility file extracting type definitions, constants, and helper functions - good code organization
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx4/5Added 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.tsx5/5Major 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

<Button
size="lg"
onClick={()=>runAsynchronouslyWithAlert(setupPayments)}
onClick={()=>setupPayments()}
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: removedrunAsynchronouslyWithAlert which was handling errors - button click now has no error handling

Suggested change
onClick={()=>setupPayments()}
onClick={()=>runAsynchronouslyWithAlert(setupPayments)}

Context Used: Rule fromdashboard - UserunAsynchronouslyWithAlert 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/layout.tsxLine: 140:140Comment:**logic:** removed`runAsynchronouslyWithAlert` which was handling errors - button click now has no error handling```suggestion                  onClick={() => runAsynchronouslyWithAlert(setupPayments)}```**Context Used:** Rule from`dashboard` - Use`runAsynchronouslyWithAlert` from`@stackframe/stack-shared/dist/utils/promises` for async butto... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +38 to +40
consthandleDisableTestMode=async()=>{
awaitproject.updateConfig({"payments.testMode":false});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: missing error handling - ifupdateConfig fails, user won't see an error message

Suggested change
consthandleDisableTestMode=async()=>{
awaitproject.updateConfig({"payments.testMode":false});
};
consthandleDisableTestMode=async()=>{
awaitrunAsynchronouslyWithAlert(async()=>{
awaitproject.updateConfig({"payments.testMode":false});
});
};

Context Used: Rule fromdashboard - UserunAsynchronouslyWithAlert 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/layout.tsxLine: 38:40Comment:**logic:** missing error handling - if`updateConfig` fails, user won't see an error message```suggestion  const handleDisableTestMode = async () => {    await runAsynchronouslyWithAlert(async () => {      await project.updateConfig({ "payments.testMode": 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.

Copy link
Contributor

@coderabbitaicoderabbitaibot 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:

constgetHeaderPadding=()=>{if(props.noSeparator)return"pt-1 pb-0";returnprops.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 between0a8e3ff and4ca9dee.

📒 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 theParameters 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
constselectOneTime=()=>{
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 +456 to +475
useEffect(()=>{
constupdateFromHash=()=>{
if(window.location.hash===currentHash)return;
setCurrentHash(window.location.hash);
};
updateFromHash();
constinterval=setInterval(()=>updateFromHash(),10);

constremoveHashTarget=()=>{
if(isHashTarget&&window.location.hash===hashAnchor){
window.history.replaceState(null,"",window.location.pathname+window.location.search);
}
};
window.addEventListener("click",removeHashTarget,{capture:true});

return()=>{
clearInterval(interval);
window.removeEventListener("click",removeHashTarget,{capture:true});
};
},[currentHash,hashAnchor,isHashTarget]);
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

10ms polling loop for hash updates is expensive; use hashchange.

Replace setInterval(10ms) with a hashchange listener.

   useEffect(() => {-    const updateFromHash = () => {-      if (window.location.hash === currentHash) return;-      setCurrentHash(window.location.hash);-    };-    updateFromHash();-    const interval = setInterval(() => updateFromHash(), 10);+    const updateFromHash = () => {+      const h = window.location.hash;+      if (h !== currentHash) setCurrentHash(h);+    };+    updateFromHash();+    window.addEventListener('hashchange', updateFromHash);@@-    return () => {-      clearInterval(interval);-      window.removeEventListener("click", removeHashTarget, { capture: true });-    };-  }, [currentHash, hashAnchor, isHashTarget]);+    return () => {+      window.removeEventListener('hashchange', updateFromHash);+      window.removeEventListener("click", removeHashTarget, { capture: true });+    };+  }, [hashAnchor, isHashTarget, currentHash]);
📝 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
useEffect(()=>{
constupdateFromHash=()=>{
if(window.location.hash===currentHash)return;
setCurrentHash(window.location.hash);
};
updateFromHash();
constinterval=setInterval(()=>updateFromHash(),10);
constremoveHashTarget=()=>{
if(isHashTarget&&window.location.hash===hashAnchor){
window.history.replaceState(null,"",window.location.pathname+window.location.search);
}
};
window.addEventListener("click",removeHashTarget,{capture:true});
return()=>{
clearInterval(interval);
window.removeEventListener("click",removeHashTarget,{capture:true});
};
},[currentHash,hashAnchor,isHashTarget]);
useEffect(()=>{
constupdateFromHash=()=>{
consth=window.location.hash;
if(h!==currentHash)setCurrentHash(h);
};
updateFromHash();
window.addEventListener('hashchange',updateFromHash);
constremoveHashTarget=()=>{
if(isHashTarget&&window.location.hash===hashAnchor){
window.history.replaceState(null,"",window.location.pathname+window.location.search);
}
};
window.addEventListener("click",removeHashTarget,{capture:true});
return()=>{
window.removeEventListener('hashchange',updateFromHash);
window.removeEventListener("click",removeHashTarget,{capture:true});
};
},[hashAnchor,isHashTarget,currentHash]);
🤖 Prompt for AI Agents
Inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxaround lines 456 to 475, there is a tight 10ms setInterval polling thewindow.location.hash; replace it with a window.addEventListener('hashchange',...) handler that updates currentHash (and still call it once initially tocapture current state), remove the interval creation/clearInterval logic, keepthe existing removeHashTarget click listener and cleanup by removing both eventlisteners in the effect return, and ensure the effect dependencies still includecurrentHash, hashAnchor, and isHashTarget.

Comment on lines +650 to +661
consthandleSaveEdit=async()=>{
consttrimmed=localProductId.trim();
constvalidId=trimmed&&/^[a-z0-9-]+$/.test(trimmed) ?trimmed :id;
if(validId!==id){
awaitonSave(validId,draft);
awaitonDelete(id);
}else{
awaitonSave(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

consthandleToggleTestMode=async(enabled:boolean)=>{
awaitproject.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/bashrg -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 filterrg -n --hidden -C2 'payments\.testMode'

Length of output: 2643


🏁 Script executed:

#!/bin/bash# Find and examine the specific filefd'page-client.tsx' apps/dashboard

Length of output: 3415


🏁 Script executed:

#!/bin/bash# Look for paymentsConfig references in dashboard payments contextrg -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 locationcat 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 handleDisableTestModecat 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 hookrg -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 bypaymentsConfig.testMode. IfupdateConfig 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 inlayout.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
consthandleToggleTestMode=async(enabled:boolean)=>{
awaitproject.updateConfig({"payments.testMode":enabled});
toast({title:enabled ?"Test mode enabled" :"Test mode disabled"});
};
consthandleToggleTestMode=async(enabled:boolean)=>{
try{
awaitproject.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
Inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxaround lines 105 to 109 and inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/layout.tsxaround lines 38 to 40, add proper error handling around calls toproject.updateConfig: wrap the async update call in try/catch, revert the localSwitch state if the update fails so UI and backend stay in sync, and surfacefailures using the alert mechanism (per design guidelines) instead of toast;ensure the catch extracts and displays the error message in the alert and thatoptimistic state changes are only applied after a successful update (or rolledback on error).

Comment on lines +116 to +123
constID_PATTERN=/^[a-z0-9-]+$/;

/**
* Validates if an ID matches the required pattern
*/
exportfunctionisValidId(id:string):boolean{
returnID_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
Inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsaround lines 116-123 (and also update lines 135-137), the ID validation regexforbids underscores while sanitizeId currently allows them, causing UIacceptance but server-side rejection; pick one canonical policy and make bothfunctions consistent: either widen ID_PATTERN to allow underscores (e.g.,include "_" in the character class) or change sanitizeId to strip/replaceunderscores to match the existing pattern, then update any related uses/tests toreflect the chosen rule so IDs are validated/sanitized consistently across thecodebase.

Comment on lines +171 to +191
exportfunctionuseQueryState(key:string,defaultValue?:string){
constgetValue=()=>newURLSearchParams(window.location.search).get(key)??defaultValue??"";

const[value,setValue]=React.useState(getValue);

React.useEffect(()=>{
constonPopState=()=>setValue(getValue());
window.addEventListener("popstate",onPopState);
return()=>window.removeEventListener("popstate",onPopState);
},[]);

constupdate=(next:string)=>{
constparams=newURLSearchParams(window.location.search);
params.set(key,next);
constnewUrl=`${window.location.pathname}?${params.toString()}`;
window.history.pushState(null,"",newUrl);
setValue(next);
};

return[value,update]asconst;
}
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

SSR-safety and history hygiene for useQueryState.

Initializer/effect read window unguarded (breaks on SSR). Also always pushState and never remove default/empty values.

Apply:

-export function useQueryState(key: string, defaultValue?: string) {-  const getValue = () => new URLSearchParams(window.location.search).get(key) ?? defaultValue ?? "";--  const [value, setValue] = React.useState(getValue);--  React.useEffect(() => {-    const onPopState = () => setValue(getValue());-    window.addEventListener("popstate", onPopState);-    return () => window.removeEventListener("popstate", onPopState);-  }, []);--  const update = (next: string) => {-    const params = new URLSearchParams(window.location.search);-    params.set(key, next);-    const newUrl = `${window.location.pathname}?${params.toString()}`;-    window.history.pushState(null, "", newUrl);-    setValue(next);-  };--  return [value, update] as const;-}+export function useQueryState(key: string, defaultValue?: string) {+  const getValue = React.useCallback(() => {+    if (!isBrowserLike()) return defaultValue ?? "";+    const params = new URLSearchParams(window.location.search);+    return params.get(key) ?? defaultValue ?? "";+  }, [key, defaultValue]);++  const [value, setValue] = React.useState<string>(getValue);++  React.useEffect(() => {+    if (!isBrowserLike()) return;+    const onPopState = () => setValue(getValue());+    window.addEventListener("popstate", onPopState);+    return () => window.removeEventListener("popstate", onPopState);+  }, [getValue]);++  const update = React.useCallback((next: string) => {+    if (!isBrowserLike()) {+      setValue(next);+      return;+    }+    if (next === value) return; // avoid redundant entries+    const params = new URLSearchParams(window.location.search);+    const isDefault = next === "" || next === (defaultValue ?? "");+    if (isDefault) {+      params.delete(key);+    } else {+      params.set(key, next);+    }+    const qs = params.toString();+    const newUrl = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;+    (isDefault ? window.history.replaceState : window.history.pushState).call(window.history, null, "", newUrl);+    setValue(next);+  }, [key, value, defaultValue]);++  return [value, update] as const;+}

Committable suggestion skipped: line range outside the PR's diff.

<>
{/* Mobile tabs */}
<divclassName="lg:hidden mb-4">
<divclassName="lg:hidden mb-4">
Copy link

Choose a reason for hiding this comment

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

Multiple JSX syntax errors with spaces in opening/closing tags. The code has< div,</div >,</Card > with spaces before or after the angle brackets, which is invalid JSX syntax and will cause compilation failures.

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.tsxindex 2d77184e..2a85dc88 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@@ -764,7 +764,7 @@ export default function PageClient() {   const innerContent = (     <>       {/* Mobile tabs */}-      < div className="lg:hidden mb-4" >+      <div className="lg:hidden mb-4">         <div className="flex space-x-1 bg-muted p-1 rounded-md">           <button             onClick={() => setActiveTab("products")}@@ -789,7 +789,7 @@ export default function PageClient() {             Items           </button>         </div>-      </div >+      </div>        {/* Content */}       <div className="flex gap-6 flex-1" style={{@@ -798,7 +798,7 @@ export default function PageClient() {       }       }>         {/* Desktop two-column layout */}-        <Card className="hidden lg:flex w-full relative" ref={containerRef} >+        <Card className="hidden lg:flex w-full relative" ref={containerRef}>           <CardContent className="flex w-full">             <div className="flex-1">               <ProductsList@@ -856,10 +856,10 @@ export default function PageClient() {               />             ))           }-        </Card >+        </Card>          {/* Mobile single column with tabs */}-        < div className="lg:hidden w-full" >+        <div className="lg:hidden w-full">           {activeTab === "products" ? (             <ProductsList               groupedProducts={groupedProducts}@@ -884,8 +884,8 @@ export default function PageClient() {               setShowItemDialog={setShowItemDialog}             />           )}-        </div >-      </div >+        </div>+      </div>     </>   );

Analysis

Invalid JSX syntax with spaces in angle brackets prevents compilation

What fails: JSX parser in page-client-list-view.tsx cannot parse tags with spaces around angle brackets like< div>,</div >,</Card > at lines 767, 792, 801, 859, 862, and 887-888

How to reproduce:

cd apps/dashboard&& pnpmexec tsc --jsx react --noEmit src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/page-client-list-view.tsx

Result: TypeScript JSX parser throws syntax errors for malformed tags. JSX requires no spaces between angle brackets and tag names.

Expected: JSX tags should be<div>,</div>,<Card> without spaces perJSX specification - spaces between< and tag names violate JSX parsing rules.

@N2D4N2D4 requested a review fromBilalG1October 28, 2025 16:57
@N2D4N2D4 removed their assignmentOct 28, 2025
value={catalogId}
onChange={(e)=>{
setCatalogId(e.target.value);
constvalue=e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g,'-');
Copy link

Choose a reason for hiding this comment

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

The input sanitization regex allows underscores but the validation regex doesn't, causing user input with underscores to pass sanitization but fail validation.

View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxindex 64b9f934..f5b91f1b 100644--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx@@ -71,7 +71,7 @@ export function CreateCatalogDialog({ open, onOpenChange, onCreate }: CreateCata                             value={catalogId}               onChange={(e) => {-                const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');+                const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-');                 setCatalogId(value);                 setErrors(prev => ({ ...prev, id: undefined }));               }}@@ -121,4 +121,3 @@ export function CreateCatalogDialog({ open, onOpenChange, onCreate }: CreateCata     </Dialog>   ); }-diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxindex b3aa62b9..fc86dc3f 100644--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx@@ -750,7 +750,7 @@ function ProductCard({ id, activeType, product, allProducts, existingItems, onSa                 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, '-');+                  const value = event.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-');                     setLocalProductId(value);                 }}                 placeholder="offer-id"@@ -1186,7 +1186,7 @@ function CatalogView({ groupedProducts, groups, existingItems, onSaveProduct, on                   ref={newGroupInputRef}                   value={newCatalogId}                   onChange={(e) => {-                    const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');+                    const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-');                     setNewCatalogId(value);                   }}                   placeholder="catalog-id"diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsindex cccdd714..4f948485 100644--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts@@ -133,5 +133,5 @@ export function generateUniqueId(prefix: string): string {  * Sanitizes user input into a valid ID format (lowercase, hyphenated)  */ export function sanitizeId(input: string): string {-  return input.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');+  return input.toLowerCase().replace(/[^a-z0-9-]/g, '-'); }

Analysis

Regex inconsistency in catalog ID sanitization vs validation causes UX confusion

What fails: CreateCatalogDialog and related components use inconsistent regex patterns - sanitization preserves underscores but validation rejects them

How to reproduce:

  1. Open create catalog dialog in dashboard
  2. Type "product_one" in Catalog ID field
  3. Observe input shows "product_one" (sanitization preserves underscore)
  4. Click "Create Catalog"
  5. Get validation error: "Catalog ID must contain only lowercase letters, numbers, and hyphens"

Result: User sees "product_one" in input field but validation fails because sanitization regex/[^a-z0-9_\-]/g preserves underscores while validation regex/^[a-z0-9-]+$/ rejects them

Expected: Consistent handling - sanitization should convert underscores to hyphens so user sees exactly what will pass validation

Files affected:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx:74
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx:753,1189
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts:136

allowedUnits={PRICE_INTERVAL_UNITS}
triggerClassName="flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-3 text-sm font-medium capitalize text-foreground shadow-sm"
onChange={(interval)=>{
savePriceUpdate();
Copy link

Choose a reason for hiding this comment

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

Suggested change
savePriceUpdate();
savePriceUpdate({
intervalSelection:interval ?(interval[0]===1 ?interval[1] :'custom') :'one-time',
intervalCount:interval ?interval[0] :1,
priceInterval:interval ?interval[1] :undefined,
});

TheonChange callback for the billing frequency IntervalPopover is not using the interval value passed to it, causing incorrect state to be saved when the user changes the billing frequency. The callback reads stale state values instead of using the provided parameter.

View Details

Analysis

IntervalPopover onChange callback ignores interval parameter causing incorrect billing frequency to be saved

What fails: ProductPriceRow IntervalPopover onChange callback inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx:186-188 ignores theinterval parameter and callssavePriceUpdate() with no arguments, causing stale state values to be saved due to React 19 automatic batching.

How to reproduce:

  1. Navigate to a project's payments/products page
  2. Edit a paid price row
  3. Change billing frequency from "monthly" to "weekly" using the IntervalPopover dropdown
  4. Check network requests - the saved interval will be[1, "month"] instead of[1, "week"]

Result: UI shows the new selection (e.g., "weekly") but backend receives old state values (e.g.,[1, "month"]) due to React batching setState calls. ThesavePriceUpdate() function readsintervalSelection,intervalCount, andpriceInterval from closure before setState updates are applied.

Expected: Should save[1, "week"] when user selects weekly billing frequency. TheonChange callback receives the correct interval parameter[1, "week"] which should be used instead of reading stale state.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@vercelvercel[bot]vercel[bot] left review comments

@coderabbitaicoderabbitai[bot]coderabbitai[bot] left review comments

Copilot code reviewCopilotCopilot left review comments

@BilalG1BilalG1Awaiting requested review from BilalG1

+1 more reviewer

@greptile-appsgreptile-apps[bot]greptile-apps[bot] left review comments

Reviewers whose approvals may not affect merge requirements

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

2 participants

@N2D4

[8]ページ先頭

©2009-2025 Movatter.jp