-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Add usage monitoring and payment prompt system #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||||||||||
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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
📒 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
UsageMonitorProvideris correctly positioned as the outermost provider, ensuring the usage monitoring context is available throughout the application. ThePaymentPromptModalis appropriately placed within the layout alongside other global UI components.
| 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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- No authentication: Any user can call this endpoint
- No input validation:
emailanduserIdare not validated and could be malicious - 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.
| } catch (error) { | ||
| console.error('Error creating checkout session:', error) | ||
| return NextResponse.json( | ||
| { error: 'Failed to create checkout session' }, | ||
| { status: 500 } | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 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.
| } 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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 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.
| 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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) | ||
| } | ||
| } | ||
| }, []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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 | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add 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.
| 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.
| const resetClickCount = () => { | ||
| setClickCount(0) | ||
| localStorage.removeItem(STORAGE_KEY) | ||
| setShowPaymentPrompt(false) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| ``` | ||
| 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] | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
incrementClickCountblindly useslocalStorageassuming it is available, which is fine in client components, but can still throw iflocalStorageaccess is blocked (e.g., certain privacy modes or SSR misconfigurations). You already guard the initial load inuseEffectimplicitly 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
resetClickCountis imported fromuseUsageMonitorbut 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
handleUpgradedoes not send any user context (such asemailoruserId) 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 themetadataandcustomer_emailwiring 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
TheUsageMonitorProvideris now wrapping all other providers and the entire app, which is logical, but it introduces a new client-side provider at the very top ofRootLayout. Given this is'use client'and useslocalStorage, 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 inhandleSubmitmeans 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-lookingSTRIPE_SECRET_KEYexample value. While it’s prefixed withsk_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.mddocumenting the new usage monitoring and payment prompt system. - Introduced a new API route
app/api/create-checkout-session/route.tsthat returns a placeholder checkout URL and documents how to integrate Stripe. - Wrapped the app in a new
UsageMonitorProvidercontext and renderedPaymentPromptModalfromapp/layout.tsxso it is globally available. - Updated
components/chat-panel.tsxto track chat submit button clicks viauseUsageMonitor().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 withlocalStoragepersistence and modal visibility.
| // 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) | ||
| } | ||
| } | ||
| }, []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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/catchto avoid breaking the app iflocalStorageis disabled or throws. - Validate that the parsed value is a finite, non-negative integer before using it.
- Optionally cap it at
CLICK_THRESHOLDto 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.
| 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This 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.
| 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' | ||
| ] | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 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.
User description
Overview
Implements efficient usage monitoring that tracks button clicks and triggers a payment prompt on the 5th click.
Changes
Features
Testing
Documentation
See
USAGE_MONITORING_GUIDE.mdfor 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
File Walkthrough
usage-monitor-context.tsx
Usage monitoring context with localStorage persistencecomponents/usage-monitor-context.tsx
threshold (default: 5)
qcx_usage_click_countpayment prompt visibility
payment-prompt-modal.tsx
Payment prompt modal with Stripe integrationcomponents/payment-prompt-modal.tsx
call-to-action
maps, and priority support
chat-panel.tsx
Chat panel click tracking integrationcomponents/chat-panel.tsx
useUsageMonitorhookincrementClickCount()inhandleSubmitto track chat submissionslayout.tsx
Root layout provider and modal integrationapp/layout.tsx
UsageMonitorProviderat root levelPaymentPromptModalcomponent to layoutroute.ts
Stripe checkout session API endpointapp/api/create-checkout-session/route.ts
variable configuration
USAGE_MONITORING_GUIDE.md
Complete usage monitoring implementation documentationUSAGE_MONITORING_GUIDE.md
configuration
additional buttons
Summary by CodeRabbit
New Features
Documentation