Skip to content

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Nov 17, 2025

User description

Overview

Implements efficient usage monitoring that tracks button clicks and triggers a payment prompt on the 5th click.

Changes

  • ✅ UsageMonitorContext for tracking clicks
  • ✅ PaymentPromptModal with Stripe integration
  • ✅ Chat submit button click tracking
  • ✅ localStorage persistence across sessions
  • ✅ Stripe checkout API endpoint
  • ✅ Comprehensive documentation

Features

  • Efficient localStorage-based tracking
  • Beautiful modal UI with upgrade benefits
  • Stripe-ready (requires configuration)
  • Non-intrusive user experience
  • Configurable click threshold (default: 5)

Testing

  1. Submit 5 messages in chat
  2. Modal appears on 5th submission
  3. Click tracking persists across page refreshes

Documentation

See USAGE_MONITORING_GUIDE.md for complete setup instructions.


PR Type

Enhancement


Description

  • Implements usage monitoring system tracking button clicks with localStorage persistence

  • Triggers payment prompt modal on 5th chat submission click

  • Adds Stripe checkout API endpoint with configuration instructions

  • Integrates PaymentPromptModal component into root layout

  • Provides comprehensive implementation guide with setup and customization instructions


Diagram Walkthrough

flowchart LR
  ChatPanel["Chat Panel"] -- "incrementClickCount()" --> UsageMonitor["Usage Monitor Context"]
  UsageMonitor -- "localStorage persistence" --> Storage["localStorage"]
  UsageMonitor -- "threshold reached" --> PaymentModal["Payment Prompt Modal"]
  PaymentModal -- "Upgrade Now" --> CheckoutAPI["Stripe Checkout API"]
  CheckoutAPI -- "redirect" --> Stripe["Stripe Checkout"]
Loading

File Walkthrough

Relevant files
Enhancement
usage-monitor-context.tsx
Usage monitoring context with localStorage persistence     

components/usage-monitor-context.tsx

  • Creates React Context for tracking button clicks with configurable
    threshold (default: 5)
  • Implements localStorage persistence under key qcx_usage_click_count
  • Provides hooks for incrementing, resetting click count and managing
    payment prompt visibility
  • Automatically triggers payment prompt when click threshold is reached
+76/-0   
payment-prompt-modal.tsx
Payment prompt modal with Stripe integration                         

components/payment-prompt-modal.tsx

  • Implements modal dialog component displaying upgrade benefits and
    call-to-action
  • Includes "Upgrade Now" button that calls Stripe checkout API endpoint
  • Provides "Maybe Later" button to dismiss modal
  • Shows feature benefits including unlimited conversations, advanced
    maps, and priority support
+115/-0 
chat-panel.tsx
Chat panel click tracking integration                                       

components/chat-panel.tsx

  • Imports and integrates useUsageMonitor hook
  • Calls incrementClickCount() in handleSubmit to track chat submissions
  • Enables usage monitoring for chat interactions
+5/-0     
layout.tsx
Root layout provider and modal integration                             

app/layout.tsx

  • Wraps application with UsageMonitorProvider at root level
  • Adds PaymentPromptModal component to layout
  • Ensures usage monitoring context is available throughout application
+29/-24 
route.ts
Stripe checkout session API endpoint                                         

app/api/create-checkout-session/route.ts

  • Creates Next.js API route for Stripe checkout session creation
  • Includes placeholder implementation with detailed setup instructions
  • Provides example code for Stripe SDK integration with environment
    variable configuration
  • Handles POST requests with optional email and userId parameters
  • Returns placeholder response with configuration guidance
+71/-0   
Documentation
USAGE_MONITORING_GUIDE.md
Complete usage monitoring implementation documentation     

USAGE_MONITORING_GUIDE.md

  • Comprehensive implementation guide covering architecture and features
  • Detailed setup instructions for Stripe integration with step-by-step
    configuration
  • Configuration options for customizing click threshold and tracking
    additional buttons
  • Testing procedures for click tracking and localStorage persistence
  • Troubleshooting section addressing common issues
  • Security considerations and performance impact analysis
+304/-0 

Summary by CodeRabbit

  • New Features

    • Added usage monitoring that tracks user interactions with a persistent counter.
    • Payment upgrade prompt now appears after 5 clicks, allowing users to upgrade for continued access.
    • Integrated Stripe checkout functionality for seamless payment processing.
  • Documentation

    • Added comprehensive setup and configuration guide for usage monitoring.

- Implement UsageMonitorContext for tracking button clicks
- Add PaymentPromptModal with Stripe integration
- Track chat submit button clicks (triggers on 5th click)
- Persist click count in localStorage across sessions
- Add Stripe checkout API endpoint
- Include comprehensive implementation guide

Features:
- Efficient localStorage-based tracking
- Beautiful modal UI with upgrade benefits
- Stripe-ready (requires configuration)
- Non-intrusive user experience
- Configurable click threshold
@vercel
Copy link

vercel bot commented Nov 17, 2025

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

Project Deployment Preview Comments Updated (UTC)
qcx Ready Ready Preview Comment Nov 17, 2025 11:26am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 17, 2025

Walkthrough

This PR implements a usage monitoring system that tracks user interactions via localStorage, displays a payment prompt modal when a click threshold is reached (default: 5), integrates Stripe checkout session creation, and modifies the app layout to wrap the provider and inject the modal component.

Changes

Cohort / File(s) Summary
Usage Monitoring Core
components/usage-monitor-context.tsx, components/payment-prompt-modal.tsx
Introduces UsageMonitorProvider context with localStorage persistence, click counting, and threshold-based payment prompt visibility. Adds PaymentPromptModal component that displays upgrade prompt and calls create-checkout-session API on upgrade action.
Integration & Layout
app/layout.tsx, components/chat-panel.tsx
Wraps layout with UsageMonitorProvider; injects PaymentPromptModal in rendering tree. Updates chat-panel to call incrementClickCount on message submission; extends ChatPanelRef interface with handleAttachmentClick method.
Backend
app/api/create-checkout-session/route.ts
New API route with POST handler that receives optional email and userId, returns placeholder response with Stripe integration guidance and commented example of actual checkout session creation.
Documentation
USAGE_MONITORING_GUIDE.md
New guide documenting usage monitoring setup, configuration (click threshold), Stripe integration steps, security considerations, and troubleshooting instructions.

Sequence Diagram

sequenceDiagram
    actor User
    participant ChatPanel as Chat Panel
    participant UsageMonitor as Usage Monitor
    participant Modal as Payment Modal
    participant API as Checkout API
    participant Stripe

    User->>ChatPanel: Submit message
    ChatPanel->>UsageMonitor: incrementClickCount()
    UsageMonitor->>UsageMonitor: Update count in localStorage
    
    alt Count reaches threshold (5)
        UsageMonitor->>Modal: Show payment prompt
        Modal->>User: Display upgrade modal
        User->>Modal: Click "Upgrade"
        Modal->>API: POST /create-checkout-session
        API->>API: Generate checkout session
        API-->>Modal: Return checkout URL
        Modal->>Stripe: Redirect to checkout
        Stripe->>User: Show payment form
    else Count below threshold
        UsageMonitor->>ChatPanel: Continue normal flow
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Context provider implementation: Verify localStorage persistence logic, threshold comparison, and state management patterns
  • Payment modal integration: Confirm proper API call handling, error management, and redirect logic for Stripe checkout
  • Chat panel integration: Ensure incrementClickCount is called at the correct point in the submission flow without disrupting existing logic
  • Layout changes: Validate provider nesting order and modal injection placement within component tree

Possibly related PRs

Suggested labels

Review effort 2/5

Poem

🐰 Five clicks and the prompt appears,
A gentle nudge for paid frontier,
With stripe integration in place,
We track the clicks with localStorage grace!
Context and modals hop along,
Payment flows make the app more strong ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a usage monitoring system with a payment prompt, which aligns with the primary focus of all file changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/usage-monitoring-payment-prompt

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.

@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Open redirect risk

Description: The checkout endpoint returns a placeholder redirect URL based on NEXT_PUBLIC_BASE_URL
without creating a real Stripe session or validating inputs, enabling open redirect
behavior and potential abuse of the payment flow (users are redirected to a non-verified
URL and could be phished if base URL is misconfigured).
route.ts [50-62]

Referred Code
// For now, return a placeholder response
// Replace this with actual Stripe integration
return NextResponse.json({ 
  url: `${baseUrl}?upgrade=true`,
  message: 'Stripe integration ready - configure your Stripe keys and price ID',
  instructions: [
    '1. Install Stripe SDK: npm install stripe',
    '2. Add STRIPE_SECRET_KEY to your environment variables',
    '3. Create a product and price in Stripe Dashboard',
    '4. Add STRIPE_PRICE_ID to your environment variables',
    '5. Uncomment the Stripe code in this file'
  ]
})
Unvalidated redirect

Description: The client blindly fetches '/api/create-checkout-session' and redirects the browser to
whatever 'url' the API returns without verification, which combined with the placeholder
API response can facilitate an open redirect to attacker-controlled pages if the backend
is compromised or misconfigured.
payment-prompt-modal.tsx [20-45]

Referred Code
const handleUpgrade = async () => {
  setIsProcessing(true)

  try {
    // Call Stripe checkout API
    const response = await fetch('/api/create-checkout-session', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    })

    const data = await response.json()

    if (data.url) {
      // Redirect to Stripe checkout
      window.location.href = data.url
    } else {
      console.error('Failed to create checkout session')
      setIsProcessing(false)
    }


 ... (clipped 5 lines)
SSR unsafe localStorage usage

Description: Reading from localStorage directly on mount without guarding for server-side rendering can
cause hydration/runtime errors in Next.js environments where window/localStorage is
unavailable, potentially exposing stack traces or causing denial of service for users.
usage-monitor-context.tsx [22-33]

Referred Code
// Load click count from localStorage on mount
useEffect(() => {
  const storedCount = localStorage.getItem(STORAGE_KEY)
  if (storedCount) {
    const count = parseInt(storedCount, 10)
    setClickCount(count)
    // Check if we should show prompt immediately
    if (count >= CLICK_THRESHOLD) {
      setShowPaymentPrompt(true)
    }
  }
}, [])
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logs: The checkout session creation endpoint does not record audit logs for payment initiation
attempts or outcomes, making it hard to reconstruct events.

Referred Code
export async function POST(request: NextRequest) {
  try {
    // Get user information from request if available
    const body = await request.json().catch(() => ({}))
    const { email, userId } = body

    // For Stripe integration via MCP, you would call the Stripe MCP server
    // This is a server-side implementation that should be configured with your Stripe keys

    // Example implementation structure:
    // 1. Use the Stripe MCP server to create a checkout session
    // 2. Configure your product/price IDs
    // 3. Set success and cancel URLs

    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'

    // Placeholder for Stripe MCP integration
    // You'll need to:
    // 1. Set up Stripe product and price in your Stripe dashboard
    // 2. Configure environment variables (STRIPE_SECRET_KEY, STRIPE_PRICE_ID)
    // 3. Use the Stripe MCP server or Stripe SDK to create the session


 ... (clipped 48 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Generic error handling: The API route returns a generic 500 without validating or handling malformed JSON, missing
required inputs, or non-OK fetch states from Stripe.

Referred Code
export async function POST(request: NextRequest) {
  try {
    // Get user information from request if available
    const body = await request.json().catch(() => ({}))
    const { email, userId } = body

    // For Stripe integration via MCP, you would call the Stripe MCP server
    // This is a server-side implementation that should be configured with your Stripe keys

    // Example implementation structure:
    // 1. Use the Stripe MCP server to create a checkout session
    // 2. Configure your product/price IDs
    // 3. Set success and cancel URLs

    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'

    // Placeholder for Stripe MCP integration
    // You'll need to:
    // 1. Set up Stripe product and price in your Stripe dashboard
    // 2. Configure environment variables (STRIPE_SECRET_KEY, STRIPE_PRICE_ID)
    // 3. Use the Stripe MCP server or Stripe SDK to create the session


 ... (clipped 48 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Console error logs: The endpoint logs full error objects to console which may include sensitive details in
production without structured or redacted logging.

Referred Code
} catch (error) {
  console.error('Error creating checkout session:', error)
  return NextResponse.json(

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Missing input validation: The POST handler accepts body fields like email and userId without validation or
authentication checks before initiating a payment flow.

Referred Code
// Get user information from request if available
const body = await request.json().catch(() => ({}))
const { email, userId } = body

// For Stripe integration via MCP, you would call the Stripe MCP server
// This is a server-side implementation that should be configured with your Stripe keys

// Example implementation structure:
// 1. Use the Stripe MCP server to create a checkout session
// 2. Configure your product/price IDs
// 3. Set success and cancel URLs

const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'

// Placeholder for Stripe MCP integration
// You'll need to:
// 1. Set up Stripe product and price in your Stripe dashboard
// 2. Configure environment variables (STRIPE_SECRET_KEY, STRIPE_PRICE_ID)
// 3. Use the Stripe MCP server or Stripe SDK to create the session

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Client-side usage tracking is insecure

The current usage tracking relies on client-side localStorage, which is insecure
as it can be easily manipulated by the user. For a robust paywall, this logic
should be moved to the server-side.

Examples:

components/usage-monitor-context.tsx [15-53]
const STORAGE_KEY = 'qcx_usage_click_count'
const CLICK_THRESHOLD = 5

export function UsageMonitorProvider({ children }: { children: ReactNode }) {
  const [clickCount, setClickCount] = useState<number>(0)
  const [showPaymentPrompt, setShowPaymentPrompt] = useState<boolean>(false)

  // Load click count from localStorage on mount
  useEffect(() => {
    const storedCount = localStorage.getItem(STORAGE_KEY)

 ... (clipped 29 lines)

Solution Walkthrough:

Before:

// components/usage-monitor-context.tsx

const STORAGE_KEY = 'qcx_usage_click_count';

function UsageMonitorProvider({ children }) {
  const [clickCount, setClickCount] = useState(0);

  useEffect(() => {
    // Load count from localStorage on mount
    const storedCount = localStorage.getItem(STORAGE_KEY);
    if (storedCount) setClickCount(parseInt(storedCount, 10));
  }, []);

  const incrementClickCount = () => {
    const newCount = clickCount + 1;
    localStorage.setItem(STORAGE_KEY, newCount.toString());
    setClickCount(newCount);
    // ... check threshold
  };
}

After:

// components/usage-monitor-context.tsx (conceptual)

function UsageMonitorProvider({ children }) {
  const { user } = useAuth(); // Assume an auth context
  const [clickCount, setClickCount] = useState(0);

  useEffect(() => {
    // Load count from a secure server endpoint
    if (user) {
      fetch('/api/usage/count').then(res => res.json()).then(data => {
        setClickCount(data.count);
      });
    }
  }, [user]);

  const incrementClickCount = async () => {
    // Increment count via an API call to the server
    const response = await fetch('/api/usage/increment', { method: 'POST' });
    const data = await response.json();
    setClickCount(data.newCount);
  };
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical architectural flaw where the client-side localStorage implementation can be easily bypassed, undermining the entire purpose of the usage-based paywall.

High
General
Handle fetch API errors properly

In handleUpgrade, add a check for response.ok after the fetch call to properly
handle non-successful HTTP responses from the server. This prevents errors when
parsing the response and improves overall error handling.

components/payment-prompt-modal.tsx [20-45]

 const handleUpgrade = async () => {
   setIsProcessing(true)
   
   try {
     // Call Stripe checkout API
     const response = await fetch('/api/create-checkout-session', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
     })
 
+    if (!response.ok) {
+      throw new Error(`Failed to create session: ${response.statusText}`)
+    }
+
     const data = await response.json()
 
     if (data.url) {
       // Redirect to Stripe checkout
       window.location.href = data.url
     } else {
-      console.error('Failed to create checkout session')
+      console.error('Failed to get checkout URL from response')
       setIsProcessing(false)
     }
   } catch (error) {
     console.error('Error creating checkout session:', error)
     setIsProcessing(false)
   }
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out missing error handling for non-2xx HTTP responses from the fetch call. Adding a response.ok check is crucial for robustly handling server-side errors and preventing the client from proceeding with a failed request.

Medium
Initialize state from localStorage safely

Refactor the state initialization to use a useState initializer function instead
of useEffect. This ensures the click count is read from localStorage only once
during the initial render, making the component more robust against potential
re-mounts.

components/usage-monitor-context.tsx [22-33]

-// Load click count from localStorage on mount
-useEffect(() => {
+const getInitialClickCount = () => {
+  if (typeof window === 'undefined') {
+    return 0
+  }
   const storedCount = localStorage.getItem(STORAGE_KEY)
-  if (storedCount) {
-    const count = parseInt(storedCount, 10)
-    setClickCount(count)
-    // Check if we should show prompt immediately
-    if (count >= CLICK_THRESHOLD) {
+  return storedCount ? parseInt(storedCount, 10) : 0
+}
+
+export function UsageMonitorProvider({ children }: { children: ReactNode }) {
+  const [clickCount, setClickCount] = useState<number>(getInitialClickCount)
+  const [showPaymentPrompt, setShowPaymentPrompt] = useState<boolean>(false)
+
+  // Show prompt on initial load if threshold is met
+  useEffect(() => {
+    if (clickCount >= CLICK_THRESHOLD) {
       setShowPaymentPrompt(true)
     }
-  }
-}, [])
+  }, [])
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion proposes a valid and idiomatic React pattern for initializing state from localStorage using a useState initializer function. While the current useEffect with an empty dependency array is unlikely to cause issues given its placement in the root layout, the suggested change is a best practice that improves robustness.

Low
Possible issue
Fix race condition in click counter

To prevent a race condition, modify incrementClickCount to read the click count
directly from localStorage before incrementing and updating the state. This
ensures the count is always based on the most recent value, especially in
multi-tab scenarios.

components/usage-monitor-context.tsx [35-47]

 const incrementClickCount = () => {
-  setClickCount((prev) => {
-    const newCount = prev + 1
-    localStorage.setItem(STORAGE_KEY, newCount.toString())
-    
-    // Trigger payment prompt on 5th click
-    if (newCount === CLICK_THRESHOLD) {
-      setShowPaymentPrompt(true)
-    }
-    
-    return newCount
-  })
+  const storedCount = localStorage.getItem(STORAGE_KEY)
+  const currentCount = storedCount ? parseInt(storedCount, 10) : 0
+  const newCount = currentCount + 1
+  localStorage.setItem(STORAGE_KEY, newCount.toString())
+  setClickCount(newCount)
+  
+  // Trigger payment prompt when threshold is met or exceeded
+  if (newCount >= CLICK_THRESHOLD) {
+    setShowPaymentPrompt(true)
+  }
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential race condition with localStorage if the app is open in multiple tabs, and proposes a more robust implementation. It also improves the logic by changing the check from === CLICK_THRESHOLD to >= CLICK_THRESHOLD, which is a good defensive practice.

Low
  • More

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/chat-panel.tsx (1)

74-112: Track clicks after successful submission, not before.

The click is currently tracked before the form submission completes. If submit(formData) fails or throws an error, the user's click count still increments, potentially triggering the payment prompt for failed actions.

Apply this diff to track clicks only after successful submission:

   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
     e.preventDefault()
     if (!input && !selectedFile) {
       return
     }
 
-    // Track button click for usage monitoring
-    incrementClickCount()
-
     const content: ({ type: 'text'; text: string } | { type: 'image'; image: string })[] = []
     if (input) {
       content.push({ type: 'text', text: input })
     }
     if (selectedFile && selectedFile.type.startsWith('image/')) {
       content.push({
         type: 'image',
         image: URL.createObjectURL(selectedFile)
       })
     }
 
     setMessages(currentMessages => [
       ...currentMessages,
       {
         id: nanoid(),
         component: <UserMessage content={content} />
       }
     ])
 
     const formData = new FormData(e.currentTarget)
     if (selectedFile) {
       formData.append('file', selectedFile)
     }
 
     setInput('')
     clearAttachment()
 
-    const responseMessage = await submit(formData)
-    setMessages(currentMessages => [...currentMessages, responseMessage as any])
+    try {
+      const responseMessage = await submit(formData)
+      setMessages(currentMessages => [...currentMessages, responseMessage as any])
+      
+      // Track button click for usage monitoring only after successful submission
+      incrementClickCount()
+    } catch (error) {
+      console.error('Submission failed:', error)
+      // Don't increment click count on failure
+    }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8718e4c and 78a7642.

📒 Files selected for processing (6)
  • USAGE_MONITORING_GUIDE.md (1 hunks)
  • app/api/create-checkout-session/route.ts (1 hunks)
  • app/layout.tsx (2 hunks)
  • components/chat-panel.tsx (3 hunks)
  • components/payment-prompt-modal.tsx (1 hunks)
  • components/usage-monitor-context.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
components/payment-prompt-modal.tsx (2)
components/usage-monitor-context.tsx (1)
  • useUsageMonitor (70-76)
components/ui/dialog.tsx (7)
  • Dialog (112-112)
  • DialogContent (117-117)
  • DialogHeader (118-118)
  • DialogTitle (120-120)
  • DialogDescription (121-121)
  • DialogFooter (119-119)
  • props (35-53)
components/chat-panel.tsx (2)
components/usage-monitor-context.tsx (1)
  • useUsageMonitor (70-76)
components/chat.tsx (3)
  • chatPanelRef (33-35)
  • Chat (22-139)
  • window (54-54)
app/layout.tsx (2)
components/usage-monitor-context.tsx (1)
  • UsageMonitorProvider (18-68)
components/payment-prompt-modal.tsx (1)
  • PaymentPromptModal (16-115)
components/usage-monitor-context.tsx (4)
components/map-toggle-context.tsx (2)
  • mapToggleState (22-34)
  • context (36-42)
components/map-loading-context.tsx (2)
  • context (20-26)
  • children (11-18)
components/map/map-context.tsx (2)
  • children (14-22)
  • MapContextType (24-30)
components/profile-toggle-context.tsx (1)
  • context (38-44)
🪛 markdownlint-cli2 (0.18.1)
USAGE_MONITORING_GUIDE.md

285-285: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (1)
app/layout.tsx (1)

62-88: LGTM: Provider integration is well-structured.

The UsageMonitorProvider is correctly positioned as the outermost provider, ensuring the usage monitoring context is available throughout the application. The PaymentPromptModal is appropriately placed within the layout alongside other global UI components.

Comment on lines +3 to +11
export async function POST(request: NextRequest) {
try {
// Get user information from request if available
const body = await request.json().catch(() => ({}))
const { email, userId } = body

// For Stripe integration via MCP, you would call the Stripe MCP server
// This is a server-side implementation that should be configured with your Stripe keys

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

Critical: Missing authentication and input validation.

This API endpoint is publicly accessible without any authentication or authorization checks, and accepts unvalidated user input. This poses security risks:

  1. No authentication: Any user can call this endpoint
  2. No input validation: email and userId are not validated and could be malicious
  3. No rate limiting: Endpoint can be abused for DoS attacks

Apply this diff to add basic authentication and validation:

 export async function POST(request: NextRequest) {
   try {
+    // Verify authentication
+    const authHeader = request.headers.get('authorization')
+    if (!authHeader || !authHeader.startsWith('Bearer ')) {
+      return NextResponse.json(
+        { error: 'Unauthorized' },
+        { status: 401 }
+      )
+    }
+
     // Get user information from request if available
     const body = await request.json().catch(() => ({}))
     const { email, userId } = body
+    
+    // Validate inputs
+    if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+      return NextResponse.json(
+        { error: 'Invalid email format' },
+        { status: 400 }
+      )
+    }

Additionally, implement rate limiting as documented in USAGE_MONITORING_GUIDE.md (line 244).

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

Comment on lines +64 to +70
} catch (error) {
console.error('Error creating checkout session:', error)
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Improve error logging for production debugging.

The current error handler logs to console but doesn't provide enough context for debugging production issues.

Apply this diff to enhance error handling:

   } catch (error) {
-    console.error('Error creating checkout session:', error)
+    console.error('Error creating checkout session:', {
+      error: error instanceof Error ? error.message : 'Unknown error',
+      stack: error instanceof Error ? error.stack : undefined,
+      timestamp: new Date().toISOString()
+    })
     return NextResponse.json(
       { error: 'Failed to create checkout session' },
       { status: 500 }
     )
   }
📝 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
} catch (error) {
console.error('Error creating checkout session:', error)
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
)
}
} catch (error) {
console.error('Error creating checkout session:', {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString()
})
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
)
}
🤖 Prompt for AI Agents
In app/api/create-checkout-session/route.ts around lines 64 to 70 the catch
block only logs to console without useful context; replace console.error with
the application's structured logger (or a portable wrapper), log the error
stack/message plus contextual info (request id, user id or session, checkout
payload) and send the same 500 JSON response; include error instanceof Error
handling to safely extract error.message and error.stack, and add a call to the
monitoring/telemetry service (Sentry/Datadog) if configured so production errors
are captured.

import { CreditCard, Sparkles } from 'lucide-react'

export function PaymentPromptModal() {
const { showPaymentPrompt, setShowPaymentPrompt, resetClickCount } = useUsageMonitor()
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove unused variable.

The resetClickCount function is destructured but never used in this component.

Apply this diff:

-  const { showPaymentPrompt, setShowPaymentPrompt, resetClickCount } = useUsageMonitor()
+  const { showPaymentPrompt, setShowPaymentPrompt } = useUsageMonitor()
📝 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 { showPaymentPrompt, setShowPaymentPrompt, resetClickCount } = useUsageMonitor()
const { showPaymentPrompt, setShowPaymentPrompt } = useUsageMonitor()
🤖 Prompt for AI Agents
In components/payment-prompt-modal.tsx around line 17, the destructured variable
`resetClickCount` from useUsageMonitor() is unused; remove it from the
destructuring assignment so only used values remain: change the const to only
extract showPaymentPrompt and setShowPaymentPrompt to eliminate the unused
variable.

Comment on lines +20 to +45
const handleUpgrade = async () => {
setIsProcessing(true)

try {
// Call Stripe checkout API
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})

const data = await response.json()

if (data.url) {
// Redirect to Stripe checkout
window.location.href = data.url
} else {
console.error('Failed to create checkout session')
setIsProcessing(false)
}
} catch (error) {
console.error('Error creating checkout session:', error)
setIsProcessing(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

Enhance error handling and user feedback.

The current implementation doesn't handle HTTP error responses or provide user feedback when the checkout session creation fails. Users will see the processing state stick or only get console errors.

Apply this diff to improve error handling:

   const handleUpgrade = async () => {
     setIsProcessing(true)
     
     try {
       // Call Stripe checkout API
       const response = await fetch('/api/create-checkout-session', {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
         },
       })
 
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`)
+      }
+
       const data = await response.json()
 
       if (data.url) {
         // Redirect to Stripe checkout
         window.location.href = data.url
       } else {
-        console.error('Failed to create checkout session')
+        console.error('Failed to create checkout session:', data.message || 'No URL returned')
+        alert('Unable to start checkout process. Please try again later.')
         setIsProcessing(false)
       }
     } catch (error) {
-      console.error('Error creating checkout session:', error)
+      console.error('Error creating checkout session:', error)
+      alert('An error occurred. Please try again later.')
       setIsProcessing(false)
     }
   }

Consider using a toast notification instead of alert() for better UX, since you already have Toaster available in the layout.

📝 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 handleUpgrade = async () => {
setIsProcessing(true)
try {
// Call Stripe checkout API
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const data = await response.json()
if (data.url) {
// Redirect to Stripe checkout
window.location.href = data.url
} else {
console.error('Failed to create checkout session')
setIsProcessing(false)
}
} catch (error) {
console.error('Error creating checkout session:', error)
setIsProcessing(false)
}
}
const handleUpgrade = async () => {
setIsProcessing(true)
try {
// Call Stripe checkout API
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
if (data.url) {
// Redirect to Stripe checkout
window.location.href = data.url
} else {
console.error('Failed to create checkout session:', data.message || 'No URL returned')
alert('Unable to start checkout process. Please try again later.')
setIsProcessing(false)
}
} catch (error) {
console.error('Error creating checkout session:', error)
alert('An error occurred. Please try again later.')
setIsProcessing(false)
}
}
🤖 Prompt for AI Agents
In components/payment-prompt-modal.tsx around lines 20 to 45, the handler
doesn't handle non-OK HTTP responses nor surface errors to the user and may
leave processing state stuck; update handleUpgrade to check response.ok before
parsing JSON, handle non-JSON or error payloads, and show a user-facing toast
(use the existing Toaster) when creating the checkout session fails (include
server error message when available); ensure setIsProcessing(false) runs in all
failure paths (use a finally block or explicit resets) and only skip resetting
when performing a redirect to the Stripe URL.

Comment on lines +23 to +33
useEffect(() => {
const storedCount = localStorage.getItem(STORAGE_KEY)
if (storedCount) {
const count = parseInt(storedCount, 10)
setClickCount(count)
// Check if we should show prompt immediately
if (count >= CLICK_THRESHOLD) {
setShowPaymentPrompt(true)
}
}
}, [])
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

Critical: Unsafe localStorage access causes hydration errors.

Direct localStorage access without checking for its availability will cause hydration mismatches and runtime errors. In Next.js with SSR, localStorage is not available during server-side rendering, even in client components.

Apply this diff to safely access localStorage:

   // Load click count from localStorage on mount
   useEffect(() => {
+    // Guard against SSR - localStorage is only available in browser
+    if (typeof window === 'undefined') return
+    
+    try {
       const storedCount = localStorage.getItem(STORAGE_KEY)
       if (storedCount) {
         const count = parseInt(storedCount, 10)
-        setClickCount(count)
+        // Validate parsed value
+        if (!isNaN(count) && count >= 0) {
+          setClickCount(count)
+        }
         // Check if we should show prompt immediately
         if (count >= CLICK_THRESHOLD) {
           setShowPaymentPrompt(true)
         }
       }
+    } catch (error) {
+      // Handle cases where localStorage is disabled (private browsing, etc.)
+      console.warn('Failed to load click count from localStorage:', error)
+    }
   }, [])
🤖 Prompt for AI Agents
In components/usage-monitor-context.tsx around lines 23 to 33, the code reads
localStorage directly which can cause hydration/runtime errors in SSR
environments; wrap the access in a runtime check (e.g., typeof window !==
"undefined" or typeof localStorage !== "undefined") and guard with try/catch to
handle exceptions, only call localStorage.getItem inside that guarded block,
parse the value safely (fallback to 0 on NaN) and then set state and the prompt
flag accordingly so no localStorage access happens during server render.

Comment on lines +35 to +47
const incrementClickCount = () => {
setClickCount((prev) => {
const newCount = prev + 1
localStorage.setItem(STORAGE_KEY, newCount.toString())

// Trigger payment prompt on 5th click
if (newCount === CLICK_THRESHOLD) {
setShowPaymentPrompt(true)
}

return newCount
})
}
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

Add error handling for localStorage operations.

The incrementClickCount function doesn't handle cases where localStorage operations might fail (e.g., private browsing mode, storage quota exceeded).

Apply this diff:

   const incrementClickCount = () => {
     setClickCount((prev) => {
       const newCount = prev + 1
-      localStorage.setItem(STORAGE_KEY, newCount.toString())
+      
+      try {
+        localStorage.setItem(STORAGE_KEY, newCount.toString())
+      } catch (error) {
+        console.warn('Failed to save click count to localStorage:', error)
+        // Continue with in-memory tracking even if localStorage fails
+      }
       
       // Trigger payment prompt on 5th click
       if (newCount === CLICK_THRESHOLD) {
         setShowPaymentPrompt(true)
       }
       
       return newCount
     })
   }
📝 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 incrementClickCount = () => {
setClickCount((prev) => {
const newCount = prev + 1
localStorage.setItem(STORAGE_KEY, newCount.toString())
// Trigger payment prompt on 5th click
if (newCount === CLICK_THRESHOLD) {
setShowPaymentPrompt(true)
}
return newCount
})
}
const incrementClickCount = () => {
setClickCount((prev) => {
const newCount = prev + 1
try {
localStorage.setItem(STORAGE_KEY, newCount.toString())
} catch (error) {
console.warn('Failed to save click count to localStorage:', error)
// Continue with in-memory tracking even if localStorage fails
}
// Trigger payment prompt on 5th click
if (newCount === CLICK_THRESHOLD) {
setShowPaymentPrompt(true)
}
return newCount
})
}
🤖 Prompt for AI Agents
In components/usage-monitor-context.tsx around lines 35 to 47, the
incrementClickCount function writes to localStorage without error handling; wrap
the localStorage.setItem call in a try/catch so storage failures (private mode,
quota) don't break state updates, log or report the error inside the catch, and
continue returning the new count (i.e., fall back to in-memory state only) while
ensuring setShowPaymentPrompt still triggers when newCount === CLICK_THRESHOLD.

Comment on lines +49 to +53
const resetClickCount = () => {
setClickCount(0)
localStorage.removeItem(STORAGE_KEY)
setShowPaymentPrompt(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

Add error handling for localStorage removal.

The resetClickCount function should also handle potential localStorage failures.

Apply this diff:

   const resetClickCount = () => {
     setClickCount(0)
-    localStorage.removeItem(STORAGE_KEY)
+    try {
+      localStorage.removeItem(STORAGE_KEY)
+    } catch (error) {
+      console.warn('Failed to remove click count from localStorage:', error)
+    }
     setShowPaymentPrompt(false)
   }
📝 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 resetClickCount = () => {
setClickCount(0)
localStorage.removeItem(STORAGE_KEY)
setShowPaymentPrompt(false)
}
const resetClickCount = () => {
setClickCount(0)
try {
localStorage.removeItem(STORAGE_KEY)
} catch (error) {
console.warn('Failed to remove click count from localStorage:', error)
}
setShowPaymentPrompt(false)
}
🤖 Prompt for AI Agents
In components/usage-monitor-context.tsx around lines 49 to 53, the
resetClickCount function calls localStorage.removeItem without guarding against
exceptions; wrap the localStorage access in a try/catch (and optionally check
for window/localStorage existence) so failures won't crash the app, log the
error (processLogger or console.error) and continue to setClickCount(0) and
setShowPaymentPrompt(false) as fallback behavior.

Comment on lines +285 to +297
```
QCX/
├── app/
│ ├── layout.tsx [MODIFIED]
│ └── api/
│ └── create-checkout-session/
│ └── route.ts [NEW]
├── components/
│ ├── chat-panel.tsx [MODIFIED]
│ ├── usage-monitor-context.tsx [NEW]
│ └── payment-prompt-modal.tsx [NEW]
└── USAGE_MONITORING_GUIDE.md [NEW]
```
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 | 🟡 Minor

Add language identifier to fenced code block.

The fenced code block representing the file structure should have a language identifier to satisfy markdown linting rules.

Apply this diff:

-```
+```plaintext
 QCX/
 ├── app/
 │   ├── layout.tsx                                    [MODIFIED]
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

285-285: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In USAGE_MONITORING_GUIDE.md around lines 285 to 297, the fenced code block
showing the file tree lacks a language identifier; update the opening fence from
``` to ```plaintext so the block starts with ```plaintext and leave the closing
fence unchanged, ensuring the code block now includes the plaintext language tag
to satisfy markdown linting.

Copy link

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

Overall the implementation is solid and matches the described feature set, but there are a few rough edges around resilience, UX, and lifecycle management. usage-monitor-context should handle corrupted localStorage data more defensively and clarify how/when click counts are reset. The payment modal’s checkout call lacks robust error handling and user feedback, and the API route is a placeholder that should more clearly signal misconfiguration to clients. None of these are type or lint issues, but addressing them will make the system more maintainable and production-ready.

Additional notes (6)
  • Maintainability | components/usage-monitor-context.tsx:35-46
    incrementClickCount blindly uses localStorage assuming it is available, which is fine in client components, but can still throw if localStorage access is blocked (e.g., certain privacy modes or SSR misconfigurations). You already guard the initial load in useEffect implicitly by running only on the client, but the write path could be slightly more defensive to avoid runtime errors in more constrained environments.

  • Maintainability | components/payment-prompt-modal.tsx:16-20
    resetClickCount is imported from useUsageMonitor but never used in this component. This is dead code and makes it harder to see what the modal is actually responsible for.

Either remove the unused symbol, or wire it into handleClose/handleUpgrade as part of the click-count lifecycle as discussed in other comments.

  • Maintainability | components/payment-prompt-modal.tsx:20-33
    handleUpgrade does not send any user context (such as email or userId) even though the checkout API route is already coded to accept it. This makes it harder to reconcile Stripe sessions with application users and wastes the metadata and customer_email wiring shown in the guide and route comments. Passing through whatever minimal identifying info you have (even if optional) will improve downstream analytics and customer support without materially increasing complexity.

  • Performance | app/layout.tsx:60-88
    The UsageMonitorProvider is now wrapping all other providers and the entire app, which is logical, but it introduces a new client-side provider at the very top of RootLayout. Given this is 'use client' and uses localStorage, this is acceptable, but you should ensure this provider ordering is intentional because it means the usage-monitor context is constructed before theme, map, and profile contexts and will participate in every app-level render. There's no urgent bug here, but if you plan more cross-cutting providers, consider whether some can be scoped more narrowly to avoid global re-render chains.

  • Readability | components/chat-panel.tsx:77-82
    Tracking clicks as the first action in handleSubmit means that users who trigger validation errors, backend failures, or other early exits after this point will still have their attempt counted as a "usage click". This may or may not match your business definition of "usage".

If you intend to count only successful message sends, you should move incrementClickCount() to after the submit call and error handling logic. If counting attempts is acceptable, consider clarifying that in the documentation and/or code comment so future maintainers don’t misinterpret the metric.

  • Security | USAGE_MONITORING_GUIDE.md:103-109
    The test data in the documentation uses a realistic-looking STRIPE_SECRET_KEY example value. While it’s prefixed with sk_test_ and clearly a placeholder, it’s safer to use an obviously dummy token (e.g. sk_test_XXXXXXXXXXXXXXXXXXXXXXXX) to avoid any confusion or accidental check-in of real keys by copy-paste. This is a small docs tweak but improves security hygiene.
Summary of changes

Summary of Changes

  • Added a comprehensive USAGE_MONITORING_GUIDE.md documenting the new usage monitoring and payment prompt system.
  • Introduced a new API route app/api/create-checkout-session/route.ts that returns a placeholder checkout URL and documents how to integrate Stripe.
  • Wrapped the app in a new UsageMonitorProvider context and rendered PaymentPromptModal from app/layout.tsx so it is globally available.
  • Updated components/chat-panel.tsx to track chat submit button clicks via useUsageMonitor().incrementClickCount.
  • Implemented components/payment-prompt-modal.tsx, a client-side dialog that shows upgrade messaging and calls the checkout API.
  • Implemented components/usage-monitor-context.tsx, a client-side React context managing click counts with localStorage persistence and modal visibility.

Comment on lines +22 to +33
// Load click count from localStorage on mount
useEffect(() => {
const storedCount = localStorage.getItem(STORAGE_KEY)
if (storedCount) {
const count = parseInt(storedCount, 10)
setClickCount(count)
// Check if we should show prompt immediately
if (count >= CLICK_THRESHOLD) {
setShowPaymentPrompt(true)
}
}
}, [])

Choose a reason for hiding this comment

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

localStorage access inside useEffect is fine for client components, but in environments like Next.js with React strict mode, this effect will run twice in development, potentially briefly showing the payment prompt when the threshold has already been exceeded. More importantly, parseInt on arbitrary stored data is not guarded against NaN or negative values, so corrupted storage could lead to unexpected behavior.

To make this more robust, consider hardening the localStorage read:

  • Wrap access in a try/catch to avoid breaking the app if localStorage is disabled or throws.
  • Validate that the parsed value is a finite, non-negative integer before using it.
  • Optionally cap it at CLICK_THRESHOLD to avoid large, useless numbers.

This improves resilience without changing the external behavior for valid data.

Suggestion

You can harden the initialization logic like this:

useEffect(() => {
  try {
    const storedCount = localStorage.getItem(STORAGE_KEY)
    if (!storedCount) return

    const count = Number.parseInt(storedCount, 10)
    if (!Number.isFinite(count) || count < 0) {
      localStorage.removeItem(STORAGE_KEY)
      return
    }

    const safeCount = Math.min(count, CLICK_THRESHOLD)
    setClickCount(safeCount)

    if (safeCount >= CLICK_THRESHOLD) {
      setShowPaymentPrompt(true)
    }
  } catch (err) {
    console.error('Failed to read usage count from localStorage', err)
  }
}, [])

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.

Comment on lines +20 to +44
const handleUpgrade = async () => {
setIsProcessing(true)

try {
// Call Stripe checkout API
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})

const data = await response.json()

if (data.url) {
// Redirect to Stripe checkout
window.location.href = data.url
} else {
console.error('Failed to create checkout session')
setIsProcessing(false)
}
} catch (error) {
console.error('Error creating checkout session:', error)
setIsProcessing(false)
}

Choose a reason for hiding this comment

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

This component blindly assumes the /api/create-checkout-session call succeeds and that response.json() will yield a valid object. In failure cases (non-2xx HTTP status, invalid JSON, network issues), you only log to the console and silently leave the user on the same screen, with no feedback.

For a payment-entry point, this is a weak UX and could be confusing. You should at least check response.ok, handle JSON parsing errors, and surface a user-visible error (e.g., a toast or inline message), while resetting isProcessing reliably via finally so the button is not stuck disabled.

Suggestion

You can make the flow more robust like this:

const handleUpgrade = async () => {
  setIsProcessing(true)

  try {
    const response = await fetch('/api/create-checkout-session', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    })

    if (!response.ok) {
      console.error('Failed to create checkout session:', response.status)
      // TODO: show user-visible error (toast/snackbar)
      return
    }

    const data: { url?: string } = await response.json().catch(err => {
      console.error('Invalid JSON from checkout session endpoint', err)
      return {}
    })

    if (data.url) {
      window.location.href = data.url
    } else {
      console.error('Checkout session response missing URL')
      // TODO: show user-visible error
    }
  } catch (error) {
    console.error('Error creating checkout session:', error)
    // TODO: show user-visible error
  } finally {
    setIsProcessing(false)
  }
}

Reply with "@CharlieHelps yes please" if you want me to add a commit with this improved error handling and placeholder UX hooks.

Comment on lines +3 to +62
export async function POST(request: NextRequest) {
try {
// Get user information from request if available
const body = await request.json().catch(() => ({}))
const { email, userId } = body

// For Stripe integration via MCP, you would call the Stripe MCP server
// This is a server-side implementation that should be configured with your Stripe keys

// Example implementation structure:
// 1. Use the Stripe MCP server to create a checkout session
// 2. Configure your product/price IDs
// 3. Set success and cancel URLs

const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'

// Placeholder for Stripe MCP integration
// You'll need to:
// 1. Set up Stripe product and price in your Stripe dashboard
// 2. Configure environment variables (STRIPE_SECRET_KEY, STRIPE_PRICE_ID)
// 3. Use the Stripe MCP server or Stripe SDK to create the session

/*
Example with Stripe SDK (install with: npm install stripe):

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)

const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: process.env.STRIPE_PRICE_ID, // e.g., 'price_1234567890'
quantity: 1,
},
],
mode: 'subscription', // or 'payment' for one-time
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/`,
customer_email: email,
metadata: {
userId: userId || 'anonymous',
},
})

return NextResponse.json({ url: session.url })
*/

// For now, return a placeholder response
// Replace this with actual Stripe integration
return NextResponse.json({
url: `${baseUrl}?upgrade=true`,
message: 'Stripe integration ready - configure your Stripe keys and price ID',
instructions: [
'1. Install Stripe SDK: npm install stripe',
'2. Add STRIPE_SECRET_KEY to your environment variables',
'3. Create a product and price in Stripe Dashboard',
'4. Add STRIPE_PRICE_ID to your environment variables',
'5. Uncomment the Stripe code in this file'
]
})

Choose a reason for hiding this comment

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

The POST handler currently ignores the incoming email and userId fields entirely and always returns a placeholder URL with an upgrade=true query parameter. That’s fine as a stub, but it may be surprising that your documentation suggests configuring Stripe here while the handler provides no guardrails.

At minimum, consider documenting in-code that this is intentionally non-production and should not be deployed as-is, or add a feature flag/env check that prevents accidental exposure of a half-configured payment endpoint in production. Also, you might want to return a non-2xx status when Stripe is not configured so the client can distinguish between a real checkout URL and a placeholder.

Suggestion

You can gate the placeholder behavior behind an env flag and signal to the client when Stripe is not configured:

export async function POST(request: NextRequest) {
  try {
    const body = await request.json().catch(() => ({}))
    const { email, userId } = body
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'

    const stripeKey = process.env.STRIPE_SECRET_KEY
    const priceId = process.env.STRIPE_PRICE_ID

    if (!stripeKey || !priceId) {
      return NextResponse.json(
        {
          error: 'Stripe not configured',
          message: 'Set STRIPE_SECRET_KEY and STRIPE_PRICE_ID to enable checkout.',
        },
        { status: 503 },
      )
    }

    // TODO: real Stripe integration here
  } catch (error) {
    console.error('Error creating checkout session:', error)
    return NextResponse.json(
      { error: 'Failed to create checkout session' },
      { status: 500 },
    )
  }
}

Reply with "@CharlieHelps yes please" if you want me to add a commit that adds this configuration guard and clearer error signaling.

@charliecreates charliecreates bot removed the request for review from CharlieHelps November 17, 2025 11:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants