Skip to content

Conversation

@drfarrell
Copy link
Collaborator

@drfarrell drfarrell commented Sep 18, 2025

Summary

  • Added a new Subscription tab to the settings panel for managing user subscriptions
  • Users can now view their current plan, upgrade/change plans, and cancel subscriptions
  • Integrated payment management with Stripe customer portal
  • Also adds in the ability to delete account

Changes

New Features

  • Subscription Tab Component (subscription-tab.tsx)

    • Displays current subscription plan (Free/Pro) with details
    • Shows scheduled changes (cancellations or plan modifications)
  • Subscription Management

    • Dropdown menu with contextual options based on subscription status
    • "Upgrade plan" for free users
    • "Change plan" for Pro users
    • "Cancel subscription" with reactivation option for cancelled subscriptions
  • Payment Management

  • Cancel Subscription Modal

    • Confirmation dialog with clear warning about losing premium features
    • UI implementation only - backend cancellation logic to be added separately

Integration

  • Added subscription tab to both project and non-project settings modals
  • Appears under "Global Settings" with credit card icon
  • Reuses existing useSubscription hook for data
  • Opens existing plan selection modal for upgrades

Screenshots

The implementation follows the design inspiration provided while maintaining consistency with Onlook's existing UI patterns.

Test Plan

  • Settings modal opens correctly
  • Subscription tab displays current plan information
  • Manage dropdown shows appropriate options based on subscription status
  • Upgrade plan button opens the pricing modal
  • Cancel subscription shows confirmation modal
  • Payment manage button opens Stripe portal in new tab
  • UI is responsive and follows dark theme

🤖 Generated with Claude Code


Important

Adds subscription management and account deletion features to the settings panel, with Stripe integration for payments and backend support for user data handling.

  • Subscription Management:
    • Adds SubscriptionTab in settings-modal for managing subscription plans and payments.
    • Integrates with Stripe for payment management.
    • Includes SubscriptionCancelModal for confirming subscription cancellations.
  • Account Deletion:
    • Adds UserDeleteSection in preferences-tab for account deletion with confirmation dialogs.
    • Implements backend logic for deleting user accounts in user.ts.
  • UI Enhancements:
    • Updates avatar-dropdown to open subscription and settings modals.
    • Renames PREFERENCES to ACCOUNT in SettingsTabValue.
  • Database Changes:
    • Adds cascading delete/update to foreign keys in deployment.ts, rate-limits.ts, subscription.ts, and usage.ts.

This description was created by Ellipsis for ec39c3b. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Subscription tab in Settings with billing portal, upgrade/change/cancel/reactivate flows and cancel-confirmation modal.
    • In-settings user deletion section with two-step confirmation and account deletion flow.
  • Changes

    • Preferences tab relabeled to "Account"; settings now open to specific tabs.
    • Default GitHub star fallback value updated.
  • Backend

    • Admin service-role support and protected user delete endpoint added.
    • Database foreign keys now cascade; Message seed adds usage field.
  • Chores

    • Added dev dependency "ai".
  • Bug Fixes

    • Improved auth error handling and clearer dev-login error messages.

Add a new Subscription tab in the settings panel that allows users to:
- View their current subscription plan and status
- Upgrade or change their subscription plan
- Cancel their subscription with a confirmation modal
- Manage billing through Stripe customer portal

The implementation includes:
- New subscription-tab component with plan management UI
- Integration with existing subscription data and modals
- Dropdown menus for subscription and payment management
- Cancel subscription confirmation dialog (UI only)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@vercel
Copy link

vercel bot commented Sep 18, 2025

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

Project Deployment Preview Comments Updated (UTC)
web Ready Ready Preview Comment Sep 23, 2025 4:28am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
docs Skipped Skipped Sep 23, 2025 4:28am

@supabase
Copy link

supabase bot commented Sep 18, 2025

This pull request has been ignored for the connected project wowaemfasoptxrdjhilu because there are no changes detected in apps/backend/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link

coderabbitai bot commented Sep 18, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

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

Walkthrough

Adds a Subscription tab and related components, simplifies Preferences to a UserDeleteSection, unifies settings modal controls, adds a user-delete API and admin Supabase client, updates env schema, introduces FK cascade constraints in DB schemas, adjusts seeded Message shape, and adds a devDependency.

Changes

Cohort / File(s) Summary of Changes
Enum & helpers
apps/web/client/src/components/ui/settings-modal/helpers.tsx
Updated SettingsTabValue: PREFERENCES value changed to 'account'; added SUBSCRIPTION = 'subscription'.
Settings modal — tabs integration
apps/web/client/src/components/ui/settings-modal/non-project.tsx, apps/web/client/src/components/ui/settings-modal/with-project.tsx
Added a Subscription tab entry (label SettingsTabValue.SUBSCRIPTION, icon CreditCard) and render <SubscriptionTab /> when selected; appended to global tabs.
Subscription UI
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx, apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx
Added SubscriptionTab component (MobX observer) with subscription/payment UI, TRPC mutation to create billing-portal sessions, manage dropdown (Upgrade/Change/Cancel/Reactivate) and SubscriptionCancelModal dialog component.
Preferences tab simplified
apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx
Replaced inlined language/theme/delete-warning UI with UserDeleteSection; removed prior API/state handling for preferences.
Settings modal — user deletion
apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx
Added UserDeleteSection (two-step confirmation) using api.user.get and api.user.delete mutation; on success shows toast, signs out and redirects.
Avatar dropdown behavior
apps/web/client/src/components/ui/avatar-dropdown/index.tsx
Replaced standalone subscription modal toggles with unified settings modal flow: sets stateManager.settingsTab = SettingsTabValue.SUBSCRIPTION (or PREFERENCES) and stateManager.isSettingsModalOpen = true.
Server API — user delete
apps/web/client/src/server/api/routers/user/user.ts
Added delete protectedProcedure mutation that deletes the authenticated user record from authUsers where id = ctx.user.id.
tRPC admin procedure
apps/web/client/src/server/api/trpc.ts
Added adminProcedure which creates an admin Supabase client and injects it into the procedure context for service-role admin operations.
Admin Supabase client & env
apps/web/client/src/utils/supabase/admin.ts, apps/web/client/src/env.ts
Added createAdminClient() factory using SUPABASE_SERVICE_ROLE_KEY; added SUPABASE_SERVICE_ROLE_KEY to server/runtime env and adjusted zod URL types for server/client values.
DB — cascade FKs
packages/db/src/schema/...
packages/db/src/schema/domain/deployment.ts, packages/db/src/schema/subscription/*.ts, packages/db/src/schema/subscription/usage.ts
Added { onDelete: 'cascade', onUpdate: 'cascade' } to several foreign key references (deployments.requestedBy; subscriptions.userId/productId/priceId; rate_limits.userId/subscriptionId; usageRecords.userId).
Seed & types
packages/db/src/seed/db.ts
Added usage: null to every seeded Message object (implies Message includes a usage field).
DevDependency
packages/db/package.json
Added devDependency "ai": "5.0.26".
Minor UI default
apps/web/client/src/app/_components/top-bar/github.tsx
Updated fallback GitHub star count default from 20550 to 22000.
Auth flow error handling
apps/web/client/src/app/auth/auth-context.tsx, apps/web/client/src/app/login/actions.tsx
Improved try/catch/finally error handling and user toasts for login/dev-login; devLogin now rethrows with original error message on failure.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Avatar as AvatarDropdown
  participant State as stateManager
  participant Modal as SettingsModal
  participant Tabs as TabsRenderer
  participant Sub as SubscriptionTab
  participant API as TRPC Backend
  participant Stripe as Stripe Billing Portal
  participant Auth as Client Auth

  User->>Avatar: Click "Subscription" or "Settings"
  Avatar->>State: settingsTab = SUBSCRIPTION / PREFERENCES\nisSettingsModalOpen = true
  State->>Modal: open with active tab
  Modal->>Tabs: render active tab
  alt Subscription tab active
    Tabs->>Sub: mount SubscriptionTab
    Sub->>API: query subscription data
    User->>Sub: Click "Manage" (billing portal)
    Sub->>API: mutation create billing portal session
    API-->>Sub: returns portalUrl
    Sub->>Stripe: open portalUrl (new tab)
    User->>Sub: Click "Cancel subscription"
    Sub->>Sub: show SubscriptionCancelModal (local)
  end
  alt Account deletion path
    Tabs->>Auth: User triggers delete via UserDeleteSection
    User->>API: call api.user.delete mutation
    API->>DB: delete authUsers where id = ctx.user.id
    API-->>Auth: mutation success
    Auth->>User: sign out & redirect to login
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hop through tabs with a curious twitch,
A Subscription button—click and switch.
Manage portals, cancel with care,
Delete prompts whispered in the air.
Rabbit nods—new tabs shine bright 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: add subscription management to settings panel" is a concise, single-sentence summary that directly reflects the primary change in the PR (adding subscription management UI to the settings panel). It avoids noisy file lists or vague wording and clearly communicates the main intent to reviewers. This aligns with the repository's title guidance and is appropriate for the changeset.
Description Check ✅ Passed The PR description is detailed and maps well to the changeset, providing a clear summary, a thorough "Changes" section (including subscription UI, account deletion, env and backend notes), and a test-plan checklist that helps reviewers understand intended behavior. However the repository template's "Related Issues" section is empty and the "Type of Change" checklist boxes were not marked, and the Testing section contains high-level checklist items rather than concrete verification steps or test coverage details. Given the overall completeness and usefulness of the information provided, the description is mostly complete but would benefit from filling the missing template fields and expanding test instructions before merge.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3de3840 and ec39c3b.

📒 Files selected for processing (1)
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (1 hunks)

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

❤️ Share

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

Comment on lines +68 to +69
subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? (
<>Pro plan (cancelling on {subscription.scheduledChange.scheduledChangeAt.toLocaleDateString()})</>
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential null pointer exception when accessing subscription.scheduledChange.scheduledChangeAt. If scheduledChange exists but scheduledChangeAt is null/undefined, calling toLocaleDateString() will throw a runtime error. Add null checking: subscription.scheduledChange?.scheduledChangeAt?.toLocaleDateString()

Suggested change
subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? (
<>Pro plan (cancelling on {subscription.scheduledChange.scheduledChangeAt.toLocaleDateString()})</>
subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? (
<>Pro plan (cancelling on {subscription.scheduledChange?.scheduledChangeAt?.toLocaleDateString()})</>

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


export const SubscriptionTab = observer(() => {
const stateManager = useStateManager();
const { data: user } = api.user.get.useQuery();
Copy link
Contributor

Choose a reason for hiding this comment

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

The 'user' data from the API query is fetched but never used. Consider removing it if not needed.

Suggested change
const { data: user } = api.user.get.useQuery();

…rtal login

- Update cancel subscription text to use text-red-200 with hover:text-red-100
- Apply same color scheme to the cancel icon with group hover state
- Simplify payment section to use a single Manage button (no dropdown)
- Implement automatic Stripe customer portal session creation
- Add loading state for portal session creation
- Use existing manageSubscription API endpoint instead of hardcoded URL

Users are now automatically logged into their Stripe billing portal without
needing to sign in again, providing a seamless experience.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@vercel vercel bot temporarily deployed to Preview – docs September 18, 2025 20:35 Inactive
Copy link

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

🧹 Nitpick comments (3)
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (3)

17-17: Use path alias for cross‑folder import.

Aligns with repo alias policy for src imports.

-import { useSubscription } from '../pricing-modal/use-subscription';
+import { useSubscription } from '@/components/ui/pricing-modal/use-subscription';

4-4: Remove unused import and query.

Avoid unnecessary network calls and dead imports.

-import { api } from '@/trpc/react';
@@
-import { ProductType, ScheduledSubscriptionAction } from '@onlook/stripe';
+import { ScheduledSubscriptionAction } from '@onlook/stripe';
@@
-    const { data: user } = api.user.get.useQuery();

Also applies to: 18-18, 24-24


71-71: Use nullish coalescing for numeric 0.

Avoids treating 0 as “Unlimited”.

-                                        <>Pro plan - {subscription?.price?.monthlyMessageLimit || 'Unlimited'} messages per month</>
+                                        <>Pro plan - {subscription?.price?.monthlyMessageLimit ?? 'Unlimited'} messages per month</>

If adopting i18n per above, use t('unlimited') and t('messagesPerMonth').

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14a3e54 and a73b440.

📒 Files selected for processing (4)
  • apps/web/client/src/components/ui/settings-modal/helpers.tsx (1 hunks)
  • apps/web/client/src/components/ui/settings-modal/non-project.tsx (2 hunks)
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (1 hunks)
  • apps/web/client/src/components/ui/settings-modal/with-project.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/components/ui/settings-modal/helpers.tsx
  • apps/web/client/src/components/ui/settings-modal/with-project.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/non-project.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/components/ui/settings-modal/helpers.tsx
  • apps/web/client/src/components/ui/settings-modal/with-project.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/non-project.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/components/ui/settings-modal/helpers.tsx
  • apps/web/client/src/components/ui/settings-modal/with-project.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/non-project.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/components/ui/settings-modal/helpers.tsx
  • apps/web/client/src/components/ui/settings-modal/with-project.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/non-project.tsx
🧬 Code graph analysis (3)
apps/web/client/src/components/ui/settings-modal/with-project.tsx (1)
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (1)
  • SubscriptionTab (22-175)
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (3)
apps/web/client/src/components/store/state/index.ts (1)
  • useStateManager (6-6)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/components/ui/pricing-modal/use-subscription.tsx (1)
  • useSubscription (7-30)
apps/web/client/src/components/ui/settings-modal/non-project.tsx (2)
packages/ui/src/components/icons/index.tsx (1)
  • Icons (138-3667)
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (1)
  • SubscriptionTab (22-175)
🔇 Additional comments (7)
apps/web/client/src/components/ui/settings-modal/with-project.tsx (2)

14-14: Import OK.

Local import is fine here; no alias needed for same‑folder modules.


72-76: Correct placement in Global Settings.

The tab wiring (label, icon, component) matches patterns used for other global tabs.

apps/web/client/src/components/ui/settings-modal/non-project.tsx (2)

11-11: Import OK.

Matches local module usage and component boundaries.


22-26: Tab wiring LGTM.

Label, icon, and component are consistent with the project modal.

apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (2)

20-20: Avoid hardcoding the Stripe billing portal URL — use a per-user Billing Portal Session or an env var (apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx:20)

Replace STRIPE_BILLING_PORTAL_URL with a backend-created per-user Stripe Billing Portal Session (recommended) or, at minimum, read the portal URL from a public env var; this prevents stale environment-specific links and reduces exposure.

Verification: an rg search for a server endpoint to create portal sessions returned "No files were searched" — cannot confirm an existing server-side endpoint; confirm whether one exists or implement a server endpoint that creates Billing Portal Sessions and update this file.


69-70: scheduledChangeAt is already a Date — no conversion required.

ScheduledChange.scheduledChangeAt is typed as Date (packages/stripe/src/types.ts); DB mappers return a Date (packages/db/src/mappers/subscription.ts); webhook/server set it with new Date(...) (apps/web/client/src/app/webhook/stripe/subscription/update.ts); tRPC uses SuperJSON on server + client (apps/web/server/src/router/trpc.ts, apps/web/client/src/server/api/trpc.ts). Calling subscription.scheduledChange.scheduledChangeAt.toLocaleDateString() in apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx is safe.

Optional: standardize date formatting (use app locale or a central formatter — several files use 'en-US').

apps/web/client/src/components/ui/settings-modal/helpers.tsx (1)

5-5: Enum addition looks good — confirm tabs list and deep-linking.

Default remains SettingsTabValue.SITE (apps/web/client/src/components/store/state/manager.ts:7). Modal openers set specific tabs (advanced-settings.tsx → SettingsTabValue.DOMAIN; custom-domain/provider.tsx → SettingsTabValue.DOMAIN; top-bar/index.tsx → SettingsTabValue.VERSIONS). Settings modal UI sets/compares stateManager.settingsTab with tab.label (apps/web/client/src/components/ui/settings-modal/with-project.tsx and non-project.tsx). Ensure apps/web/client/src/components/ui/settings-modal/helpers.tsx includes SettingsTabValue.SUBSCRIPTION in the tabs array and that any URL/query deep-link parsing maps the "subscription" value to the new enum.

Comment on lines 46 to 49
const handleManageBilling = () => {
window.open(STRIPE_BILLING_PORTAL_URL, '_blank');
setIsPaymentDropdownOpen(false);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add noopener/noreferrer to window.open.

Prevents reverse‑tabnabbing and removes opener reference.

-    const handleManageBilling = () => {
-        window.open(STRIPE_BILLING_PORTAL_URL, '_blank');
+    const handleManageBilling = () => {
+        window.open(STRIPE_BILLING_PORTAL_URL, '_blank', 'noopener,noreferrer');
         setIsPaymentDropdownOpen(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 handleManageBilling = () => {
window.open(STRIPE_BILLING_PORTAL_URL, '_blank');
setIsPaymentDropdownOpen(false);
};
const handleManageBilling = () => {
window.open(STRIPE_BILLING_PORTAL_URL, '_blank', 'noopener,noreferrer');
setIsPaymentDropdownOpen(false);
};
🤖 Prompt for AI Agents
In apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx around
lines 46-49, the call to window.open(STRIPE_BILLING_PORTAL_URL, '_blank') should
be hardened to prevent reverse-tabnabbing by either passing the feature string
'noopener,noreferrer' as the third argument (window.open(url, '_blank',
'noopener,noreferrer')) or by assigning newWindow.opener = null on the returned
window object; update the code to open the URL with noopener and noreferrer and
keep the existing setIsPaymentDropdownOpen(false) behavior.

- Change avatar dropdown "Subscription" button behavior
- Now opens settings modal directly to the subscription tab
- Users can manage their subscription from the unified settings UI
- Provides better UX by consolidating subscription management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@vercel vercel bot temporarily deployed to Preview – docs September 18, 2025 20:36 Inactive
- Fix settings dropdown to navigate to Preferences tab (first tab) by default
- Only subscription dropdown navigates to Subscription tab
- Capitalize "Subscription" in cancel modal button text
- Add proper spacing between modal buttons (gap-3)
- Improve cancel button hover state with red background and brighter text
- Use custom styling instead of destructive variant for better UX

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Add Delete Account section below Payment section
- Implement multi-step confirmation process matching ChatGPT's flow
- First modal requires user to enter their email and type "DELETE"
- Button remains locked until both validations pass
- Second modal provides final confirmation before deletion
- Include comprehensive warning list about deletion consequences
- Add proper form validation and state management
- UI only - backend deletion logic to be implemented separately

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@vercel vercel bot temporarily deployed to Preview – docs September 18, 2025 21:08 Inactive
Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
apps/web/client/src/components/ui/avatar-dropdown/index.tsx (1)

55-61: Externalize user‑facing labels to next‑intl

Avoid hardcoded strings in the BUTTONS array per repo i18n guideline. Example patch:

+import { useTranslations } from 'next-intl';
@@
 export const CurrentUserAvatar = ({ className }: { className?: string }) => {
+    const t = useTranslations('avatarMenu');
@@
-    const BUTTONS = [
+    const BUTTONS = [
         {
-            label: 'Subscription',
+            label: t('subscription'),
             icon: Icons.CreditCard,
             onClick: handleOpenSubscription,
         },
         {
-            label: 'Settings',
+            label: t('settings'),
             icon: Icons.Gear,
             onClick: handleOpenSettings,
         },
         {
-            label: 'Send Feedback',
+            label: t('sendFeedback'),
             icon: Icons.MessageSquare,
             onClick: () => {
                 void openFeedbackWidget();
                 setOpen(false);
             },
         },
         {
-            label: 'Sign Out',
+            label: t('signOut'),
             icon: Icons.Exit,
             onClick: handleSignOut,
         },
     ];

Also convert the user header and any other visible strings in this menu to translations.

Also applies to: 62-79

♻️ Duplicate comments (6)
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (6)

22-22: Remove unused user query

user isn’t used; drop the query to avoid unnecessary network work.

-    const { data: user } = api.user.get.useQuery();
+    // removed unused user query

34-39: Add user‑visible error feedback for portal failures

Console logging only is poor UX. Show a toast on error (and possibly success), wired into i18n.

-        onError: (error) => {
-            console.error('Failed to create portal session:', error);
-        },
+        onError: (error) => {
+            console.error('Failed to create portal session:', error);
+            // import { toast } from 'sonner' (or project toast)
+            // toast.error(t('payment.portalError'));
+        },

28-33: Harden window.open to prevent reverse‑tabnabbing

Open new tab with noopener/noreferrer.

-        onSuccess: (session) => {
+        onSuccess: (session) => {
             if (session?.url) {
-                window.open(session.url, '_blank');
+                window.open(session.url, '_blank', 'noopener,noreferrer');
             }
         },

81-86: Null‑safe date formatting for scheduled cancellation

scheduleChangeAt may be null/undefined; current code can throw at runtime.

-    <>Pro plan (cancelling on {subscription.scheduledChange.scheduledChangeAt.toLocaleDateString()})</>
+    <>Pro plan (cancelling on {subscription.scheduledChange?.scheduledChangeAt?.toLocaleDateString()})</>

Optional: provide a fallback if the date is absent (e.g., “cancelling soon” via i18n).


118-126: “Reactivate” label calls cancel handler

Label toggles to “Reactivate subscription” but still calls the cancel flow.

-                                {isPro && (
-                                    <DropdownMenuItem 
-                                        onClick={handleCancelSubscription}
-                                        className="cursor-pointer text-red-200 hover:text-red-100 group"
-                                    >
-                                        <Icons.CrossS className="mr-2 h-4 w-4 text-red-200 group-hover:text-red-100" />
-                                        {subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? 'Reactivate subscription' : 'Cancel subscription'}
-                                    </DropdownMenuItem>
-                                )}
+                                {isPro && (
+                                    <DropdownMenuItem
+                                        onClick={
+                                            subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION
+                                                ? handleReactivateSubscription
+                                                : handleCancelSubscription
+                                        }
+                                        className={`cursor-pointer ${
+                                            subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION
+                                                ? ''
+                                                : 'text-red-200 hover:text-red-100 group'
+                                        }`}
+                                    >
+                                        <Icons.CrossS className="mr-2 h-4 w-4 text-red-200 group-hover:text-red-100" />
+                                        {subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION
+                                            ? 'Reactivate subscription'
+                                            : 'Cancel subscription'}
+                                    </DropdownMenuItem>
+                                )}

Add the handler and an isCancelling helper:

@@
-    const [isLoadingPortal, setIsLoadingPortal] = useState(false);
+    const [isLoadingPortal, setIsLoadingPortal] = useState(false);
+    const isCancelling =
+        subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION;
@@
     const handleCancelSubscription = () => {
         setShowCancelModal(true);
         setIsManageDropdownOpen(false);
     };
+
+    const handleReactivateSubscription = () => {
+        // Reactivation logic (API) to be implemented
+        setIsManageDropdownOpen(false);
+    };

70-74: Internationalize all user‑facing strings (next‑intl)

Replace headings, descriptions, button/menu labels, and dialog copy with translations per repo policy.

Example patch (apply pattern throughout):

+import { useTranslations } from 'next-intl';
@@
 export const SubscriptionTab = observer(() => {
+    const t = useTranslations('settings.subscription');
@@
-    <h2 className="text-title3 mb-2">Subscription</h2>
-    <p className="text-muted-foreground text-small">Manage your subscription plan and billing</p>
+    <h2 className="text-title3 mb-2">{t('title')}</h2>
+    <p className="text-muted-foreground text-small">{t('subtitle')}</p>
@@
-    <p className="text-regularPlus font-medium">Current Plan</p>
+    <p className="text-regularPlus font-medium">{t('currentPlan')}</p>
@@
-    'You are currently on the Free plan'
+    {t('freeStatus')}
@@
-    <Button variant="outline" size="sm">Manage<Icons.ChevronDown className="ml-1 h-3 w-3" /></Button>
+    <Button variant="outline" size="sm">{t('manage')}<Icons.ChevronDown className="ml-1 h-3 w-3" /></Button>
@@
-    Upgrade plan
+    {t('upgrade')}
@@
-    Change plan
+    {t('change')}
@@
-    Reactivate subscription
+    {t('reactivate')}
@@
-    Cancel subscription
+    {t('cancel')}
@@
-    <p className="text-regularPlus font-medium">Payment</p>
+    <p className="text-regularPlus font-medium">{t('payment.title')}</p>
@@
-    Manage your payment methods and billing details
+    {t('payment.subtitle')}
@@
-    {isLoadingPortal ? 'Opening...' : 'Manage'}
+    {isLoadingPortal ? t('opening') : t('manage')}
@@
-    <DialogTitle>Cancel Subscription</DialogTitle>
+    <DialogTitle>{t('cancel.title')}</DialogTitle>
@@
-    Are you sure you want to cancel your subscription? You'll lose access to all premium features at the end of your current billing period.
+    {t('cancel.description')}
@@
-    Keep Subscription
+    {t('cancel.keep')}
@@
-    Cancel Subscription
+    {t('cancel.confirm')}

Also applies to: 79-90, 94-98, 100-117, 133-149, 157-176

🧹 Nitpick comments (4)
apps/web/client/src/components/ui/avatar-dropdown/index.tsx (2)

22-22: Use path alias instead of relative import

Repo guideline: apps/web/client/src/* should import via @/* or ~/*. Replace the relative helpers import.

-import { SettingsTabValue } from '../settings-modal/helpers';
+import { SettingsTabValue } from '@/components/ui/settings-modal/helpers';

84-90: Button should be type="button" and have an accessible label

Prevents accidental form submits and improves a11y. Use i18n for the label.

-                <button>
+                <button type="button" aria-label={t('openAccountMenu')}>
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (2)

85-86: Use nullish coalescing for limits

Avoid treating 0 as “Unlimited”.

-    <>Pro plan - {subscription?.price?.monthlyMessageLimit || 'Unlimited'} messages per month</>
+    <>Pro plan - {subscription?.price?.monthlyMessageLimit ?? 'Unlimited'} messages per month</>

17-18: Use path alias for internal import

Follow repo import policy for src/*.

-import { useSubscription } from '../pricing-modal/use-subscription';
+import { useSubscription } from '@/components/ui/pricing-modal/use-subscription';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a73b440 and f0b642d.

📒 Files selected for processing (2)
  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx (2 hunks)
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Avoid hardcoded user-facing text; use next-intl messages/hooks

Applied to files:

  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/**/*.{ts,tsx} : Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Applied to files:

  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
🧬 Code graph analysis (1)
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (3)
apps/web/client/src/components/store/state/index.ts (1)
  • useStateManager (6-6)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/components/ui/pricing-modal/use-subscription.tsx (1)
  • useSubscription (7-30)
🔇 Additional comments (2)
apps/web/client/src/components/ui/avatar-dropdown/index.tsx (2)

44-46: Good: unified settings modal navigation works

Switching to settingsTab + isSettingsModalOpen is cleaner than a separate subscription modal flag.


50-52: Good: direct navigation to Preferences tab

Consistent with the new tabbed settings UX.

setDeleteConfirmText('');
};

const canProceedWithDelete = deleteEmail === user?.email && deleteConfirmText === 'DELETE';
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider trimming and normalizing the account email and confirmation input to avoid input mismatches in the deletion check.

Suggested change
const canProceedWithDelete = deleteEmail === user?.email && deleteConfirmText === 'DELETE';
const canProceedWithDelete = deleteEmail.trim().toLowerCase() === user?.email?.trim().toLowerCase() && deleteConfirmText.trim() === 'DELETE';

setShowFinalDeleteConfirm(true);
};

const handleFinalDeleteAccount = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

handleFinalDeleteAccount currently only logs the deletion request. Implement the actual API call and error handling for account deletion.

@drfarrell
Copy link
Collaborator Author

@Kitenite I setup the UI for this + the UI leading up to it. I think you'll need to do the heavy-lifting on the connecting + deleting of accounts, plus perhaps validating the subscription stuff.

Comment on lines 33 to 40
const handleFinalDeleteAccount = async () => {
await deleteUser();
setShowFinalDeleteConfirm(false);
// Reset form
setDeleteEmail('');
setDeleteConfirmText('');
handleDeleteSuccess();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

The handleFinalDeleteAccount function doesn't properly handle potential errors from the deleteUser mutation. Currently, even if the account deletion fails, the code will still proceed to sign the user out and redirect them via handleDeleteSuccess().

Consider adding error handling:

const handleFinalDeleteAccount = async () => {
  try {
    await deleteUser();
    setShowFinalDeleteConfirm(false);
    setDeleteEmail('');
    setDeleteConfirmText('');
    handleDeleteSuccess();
  } catch (error) {
    toast.error('Failed to delete account. Please try again.');
  }
};

Alternatively, utilize the mutation's built-in callbacks:

const { mutate: deleteUser } = api.user.delete.useMutation({
  onSuccess: () => {
    setShowFinalDeleteConfirm(false);
    setDeleteEmail('');
    setDeleteConfirmText('');
    handleDeleteSuccess();
  },
  onError: () => {
    toast.error('Failed to delete account. Please try again.');
  }
});
Suggested change
const handleFinalDeleteAccount = async () => {
await deleteUser();
setShowFinalDeleteConfirm(false);
// Reset form
setDeleteEmail('');
setDeleteConfirmText('');
handleDeleteSuccess();
};
const handleFinalDeleteAccount = async () => {
try {
await deleteUser();
setShowFinalDeleteConfirm(false);
// Reset form
setDeleteEmail('');
setDeleteConfirmText('');
handleDeleteSuccess();
} catch (error) {
toast.error('Failed to delete account. Please try again.');
}
};

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

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

🧹 Nitpick comments (7)
apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx (3)

1-1: Drop unused tRPC import (after removing hooks)

Keeps the module clean and avoids linter noise.

- import { api } from '@/trpc/react';

8-12: Avoid rendering an empty wrapper

Empty padding-only containers create layout noise. Either return null or render a localized placeholder later.

-    return (
-        <div className="flex flex-col gap-8 p-6">
-
-        </div>
-    );
+    return null;

Note: If you prefer a placeholder, use next-intl (e.g., t('preferences.empty')) instead of hardcoded text.


2-4: Remove unnecessary MobX observer wrapper

No observable state is used here; the observer adds overhead without benefit.

-import { observer } from 'mobx-react-lite';
+// (observer not needed here)
 
-export const PreferencesTab = observer(() => {
+export const PreferencesTab = () => {
@@
-});
+};

Also applies to: 13-13

apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx (1)

56-60: Replace hardcoded strings with next‑intl

Repo guideline: avoid hardcoded UI text in apps/web/client/src/**/*.tsx. Use useTranslations and message keys.

Example:

+import { useTranslations } from 'next-intl';
@@
-export const UserDeleteSection = observer(() => {
+export const UserDeleteSection = observer(() => {
+    const t = useTranslations('settings.deleteAccount');
@@
-    <p className="text-regularPlus font-medium">Delete Account</p>
+    <p className="text-regularPlus font-medium">{t('title')}</p>
@@
-    <DialogTitle>Delete account - are you sure?</DialogTitle>
+    <DialogTitle>{t('confirmTitle')}</DialogTitle>

Also applies to: 71-86, 90-109, 111-129, 138-141, 153-160

apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx (2)

12-36: Internationalize modal copy

Replace literals with next‑intl strings to meet repo guidelines.

+import { useTranslations } from 'next-intl';
@@
-export const SubscriptionCancelModal = ({ open, onOpenChange, onConfirmCancel }: SubscriptionCancelModalProps) => {
+export const SubscriptionCancelModal = ({ open, onOpenChange, onConfirmCancel }: SubscriptionCancelModalProps) => {
+    const t = useTranslations('settings.subscription.cancel');
@@
-                    <DialogTitle>Cancel Subscription</DialogTitle>
+                    <DialogTitle>{t('title')}</DialogTitle>
@@
-                        Are you sure you want to cancel your subscription? You'll lose access to all premium features at the end of your current billing period.
+                        {t('description')}
@@
-                        Keep Subscription
+                        {t('keep')}
@@
-                        Cancel Subscription
+                        {t('confirm')}

30-36: Optional: use a destructive button style for the confirm action

If your UI kit supports variant="destructive", prefer that over custom red classes for consistency.

-                    <Button
-                        variant="outline"
+                    <Button
+                        variant="destructive"
                         onClick={onConfirmCancel}
-                        className="order-1 sm:order-2 text-red-200 hover:text-red-100 hover:bg-red-500/10 border-red-200 hover:border-red-100"
+                        className="order-1 sm:order-2"
                     >
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (1)

82-86: Null‑safe date access and correct nullish handling

Guard scheduledChangeAt and use ?? so 0 isn’t treated as “Unlimited”.

-                                        <>Pro plan (cancelling on {subscription.scheduledChange.scheduledChangeAt.toLocaleDateString()})</>
+                                        <>Pro plan (cancelling on {subscription.scheduledChange?.scheduledChangeAt?.toLocaleDateString()})</>
@@
-                                        <>Pro plan - {subscription?.price?.monthlyMessageLimit || 'Unlimited'} messages per month</>
+                                        <>Pro plan - {subscription?.price?.monthlyMessageLimit ?? 'Unlimited'} messages per month</>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0b642d and 231d873.

📒 Files selected for processing (6)
  • apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx (1 hunks)
  • apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx (1 hunks)
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (1 hunks)
  • apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx (1 hunks)
  • apps/web/client/src/server/api/routers/user/user.ts (1 hunks)
  • packages/db/package.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/client/src/server/api/routers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/server/api/routers/**/*.ts: Place tRPC routers under apps/web/client/src/server/api/routers/**
Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod
Return plain objects/arrays; rely on SuperJSON for serialization in tRPC procedures

apps/web/client/src/server/api/routers/**/*.ts: Place tRPC routers under src/server/api/routers/**
Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod
Return plain objects/arrays; rely on SuperJSON for serialization

Files:

  • apps/web/client/src/server/api/routers/user/user.ts
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/server/api/routers/user/user.ts
  • apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/server/api/routers/user/user.ts
  • apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/server/api/routers/user/user.ts
  • apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx
  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
  • apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Avoid hardcoded user-facing text; use next-intl messages/hooks

Applied to files:

  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/**/*.{ts,tsx} : Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Applied to files:

  • apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
🧬 Code graph analysis (4)
apps/web/client/src/server/api/routers/user/user.ts (1)
apps/web/client/src/server/api/trpc.ts (1)
  • protectedProcedure (130-149)
apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx (5)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
packages/ui/src/components/sonner.tsx (1)
  • toast (19-19)
apps/web/client/src/utils/constants/index.ts (1)
  • Routes (1-29)
packages/ui/src/components/button.tsx (1)
  • Button (57-57)
packages/ui/src/components/dialog.tsx (6)
  • Dialog (109-109)
  • DialogContent (111-111)
  • DialogHeader (114-114)
  • DialogTitle (117-117)
  • DialogDescription (112-112)
  • DialogFooter (113-113)
apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx (2)
packages/ui/src/components/dialog.tsx (6)
  • Dialog (109-109)
  • DialogContent (111-111)
  • DialogHeader (114-114)
  • DialogTitle (117-117)
  • DialogDescription (112-112)
  • DialogFooter (113-113)
packages/ui/src/components/button.tsx (1)
  • Button (57-57)
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (5)
apps/web/client/src/components/store/state/index.ts (1)
  • useStateManager (6-6)
apps/web/client/src/components/ui/pricing-modal/use-subscription.tsx (1)
  • useSubscription (7-30)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx (1)
  • UserDeleteSection (15-166)
apps/web/client/src/components/ui/settings-modal/subscription-cancel-modal.tsx (1)
  • SubscriptionCancelModal (12-41)
🔇 Additional comments (6)
packages/db/package.json (1)

32-34: Don't remove the "ai" devDependency from packages/db — it's referenced for types.

packages/db/src/schema/chat/message.ts contains import type { LanguageModelUsage } from 'ai'; so the devDependency is used for TypeScript type-checking; keep it (or consolidate to the workspace root only if you intend to share it across packages). Also declared in packages/models, packages/ai, packages/ui, and apps/web/client.

Likely an incorrect or invalid review comment.

apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx (1)

49-49: Normalize inputs for safer matching

Trim and case‑fold to avoid false negatives when the user types their email.

-    const canProceedWithDelete = deleteEmail === user?.email && deleteConfirmText === 'DELETE';
+    const canProceedWithDelete =
+        deleteEmail.trim().toLowerCase() === (user?.email?.trim().toLowerCase() ?? '') &&
+        deleteConfirmText.trim() === 'DELETE';
apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx (3)

70-74: Internationalize user‑facing strings

Replace literals with next‑intl (title, subtitles, labels, menu items, button text). This is a repo rule for apps/web/client/src/**/*.

Example (partial):

+import { useTranslations } from 'next-intl';
@@
 export const SubscriptionTab = observer(() => {
+    const t = useTranslations('settings.subscription');
@@
-    <h2 className="text-title3 mb-2">Subscription</h2>
+    <h2 className="text-title3 mb-2">{t('title')}</h2>
@@
-    Manage
+    {t('manage')}

Also applies to: 79-90, 94-107, 115-116, 136-148


28-36: Harden window.open and surface user feedback on errors

Add noopener/noreferrer to avoid reverse‑tabnabbing and show a toast on failure.

-    const manageSubscriptionMutation = api.subscription.manageSubscription.useMutation({
+    const manageSubscriptionMutation = api.subscription.manageSubscription.useMutation({
         onSuccess: (session) => {
             if (session?.url) {
-                window.open(session.url, '_blank');
+                const win = window.open(session.url, '_blank', 'noopener,noreferrer');
+                if (!win) window.location.href = session.url; // popup blocked fallback
             }
         },
-        onError: (error) => {
-            console.error('Failed to create portal session:', error);
-        },
+        onError: (error) => {
+            console.error('Failed to create portal session:', error);
+            toast.error('Unable to open billing portal. Please try again.');
+        },

Add missing import:

+import { toast } from '@onlook/ui/sonner';

118-126: Wrong handler for “Reactivate subscription”; label and action diverge

Call a reactivation handler when cancellation is scheduled, and avoid destructive styling for reactivation.

+    const isCancelling = subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION;
@@
-    const handleCancelSubscription = () => {
+    const handleCancelSubscription = () => {
         setShowCancelModal(true);
         setIsManageDropdownOpen(false);
     };
+
+    const handleReactivateSubscription = () => {
+        // TODO: wire reactivation mutation
+        setIsManageDropdownOpen(false);
+        toast.info('Reactivation coming soon');
+    };
@@
-                                {isPro && (
+                                {isPro && (
                                     <DropdownMenuItem
-                                        onClick={handleCancelSubscription}
-                                        className="cursor-pointer text-red-200 hover:text-red-100 group"
+                                        onClick={isCancelling ? handleReactivateSubscription : handleCancelSubscription}
+                                        className={`cursor-pointer group ${isCancelling ? '' : 'text-red-200 hover:text-red-100'}`}
                                     >
                                         <Icons.CrossS className="mr-2 h-4 w-4 text-red-200 group-hover:text-red-100" />
-                                        {subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? 'Reactivate subscription' : 'Cancel subscription'}
+                                        {isCancelling ? 'Reactivate subscription' : 'Cancel subscription'}
                                     </DropdownMenuItem>
                                 )}

Also applies to: 24-27, 48-56

apps/web/client/src/server/api/routers/user/user.ts (1)

98-101: Cannot verify ON DELETE CASCADE — confirm DB cascade & PII cleanup

Sandbox repo search returned no matches; I cannot confirm any foreign-key ON DELETE CASCADE rules for users. If the DB doesn't cascade, deleting auth (supabase.admin.deleteUser) will orphan rows and retain PII — validate soft vs hard delete per GDPR/CCPA.

File: apps/web/client/src/server/api/routers/user/user.ts — delete mutation.

Suggested checks (run from repo root):

# search repo for FK refs and cascade rules
rg -nP 'references\s+users\b' -S -C2
rg -nP 'on\s+delete\s+cascade' -S -C2
rg -nP 'foreign key\s*\(.*\)\s*references\s*users' -S -C2
# search common schema/migration files if present
fd -e sql -e prisma -e yml -e yaml -e toml -E node_modules -x rg -nP 'references\s+users\b|on\s+delete\s+cascade' -S -C2
-- Postgres DB-level check (psql)
SELECT conname, conrelid::regclass AS child_table, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE contype = 'f' AND pg_get_constraintdef(oid) ILIKE '%REFERENCES users%';

import { Icons } from '@onlook/ui/icons';
import { observer } from 'mobx-react-lite';

export const PreferencesTab = observer(() => {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Confirm client boundary exists for this tab

tRPC React hooks (and MobX observer if retained) require a client component boundary above. Verify the parent settings modal or entry file has 'use client'.

Run:


🏁 Script executed:

#!/bin/bash
# Verify a client boundary exists in settings modal tree
rg -n --type=ts,tsx -C1 $'^[ \t]*[\'"]use client[\'"]' apps/web/client/src/components/ui/settings-modal
# Also check modal entry points mentioned in the PR
rg -n --type=ts,tsx -C1 $'^[ \t]*[\'"]use client[\'"]' apps/web/client/src/components/ui/settings-modal/{non-project.tsx,with-project.tsx} 2>/dev/null || true

Length of output: 30


🏁 Script executed:

#!/bin/bash
# Verify client boundary exists in settings modal tree (search for 'use client' in ts/tsx)
dir="apps/web/client/src/components/ui/settings-modal"
echo "Checking directory: $dir"
if [ ! -d "$dir" ]; then
  echo "DIR_NOT_FOUND"
  exit 0
fi
echo "Listing files in $dir"
ls -la "$dir"
echo "Searching for 'use client' in .ts/.tsx under $dir"
grep -nR --include='*.tsx' --include='*.ts' -E "^\s*['\"]use client['\"]" "$dir" -n -H -C1 || true
echo "Also checking specific entry files (non-project.tsx, with-project.tsx)"
for f in non-project.tsx with-project.tsx; do
  p="$dir/$f"
  if [ -f "$p" ]; then
    echo "---- $p (first 120 lines) ----"
    sed -n '1,120p' "$p"
    echo "grep result:"
    grep -nE "^\s*['\"]use client['\"]" "$p" || true
  else
    echo "MISSING: $p"
  fi
done

Length of output: 11899


Add 'use client' to the settings modal entry components

PreferencesTab is a MobX observer (and may use tRPC hooks); the modal entry files lack a client boundary—add a top-line "use client" to one of these entry components (or otherwise ensure a higher client boundary):

  • apps/web/client/src/components/ui/settings-modal/non-project.tsx
  • apps/web/client/src/components/ui/settings-modal/with-project.tsx
🤖 Prompt for AI Agents
In apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx around
line 4, the PreferencesTab is a MobX observer and may use client-only hooks but
the modal entry components lack a client boundary; add a top-line "use client"
directive to one of the entry components (preferably both) —
apps/web/client/src/components/ui/settings-modal/non-project.tsx and
apps/web/client/src/components/ui/settings-modal/with-project.tsx — or ensure a
higher-level parent is already a client component, so the observer and any
tRPC/hooks run in a client boundary.

const [deleteEmail, setDeleteEmail] = useState('');
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const [showFinalDeleteConfirm, setShowFinalDeleteConfirm] = useState(false);
const { mutate: deleteUser } = api.user.delete.useMutation();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: using mutate (non‑Promise) with await; flow proceeds even if delete fails

useMutation().mutate isn’t async; await deleteUser() is a no‑op. Use mutateAsync, add error handling, and disable while loading.

Apply:

-    const { mutate: deleteUser } = api.user.delete.useMutation();
+    const { mutateAsync: deleteUser, isLoading: isDeleting } = api.user.delete.useMutation();

@@
-    const handleFinalDeleteAccount = async () => {
-        await deleteUser();
-        setShowFinalDeleteConfirm(false);
-        // Reset form
-        setDeleteEmail('');
-        setDeleteConfirmText('');
-        handleDeleteSuccess();
-    };
+    const handleFinalDeleteAccount = async () => {
+        try {
+            await deleteUser();
+            setShowFinalDeleteConfirm(false);
+            // Reset form
+            setDeleteEmail('');
+            setDeleteConfirmText('');
+            await handleDeleteSuccess();
+        } catch (e) {
+            toast.error('Failed to delete account. Please try again.');
+        }
+    };

@@
-                        <Button
+                        <Button
                             onClick={handleDeleteConfirm}
-                            disabled={!canProceedWithDelete}
+                            disabled={!canProceedWithDelete || isDeleting}
                             className="order-1 sm:order-2 bg-red-600 hover:bg-red-700 text-white disabled:bg-gray-300 disabled:text-gray-500"
                         >
-                            {canProceedWithDelete ? 'Delete Account' : 'Locked'}
+                            {isDeleting ? 'Deleting…' : (canProceedWithDelete ? 'Delete Account' : 'Locked')}
                         </Button>

Also applies to: 33-40, 123-129

🤖 Prompt for AI Agents
In apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx
around lines 21 (and similarly at 33-40, 123-129), the code uses
api.user.delete.useMutation()'s mutate (non-Promise) with await which is
ineffective; replace mutate with mutateAsync from the hook, wire loading/error
state from the mutation (isLoading/isError) to disable the delete button and
show feedback, and wrap the mutateAsync call in a try/catch to handle and
log/display errors so the flow only proceeds on successful deletion.

@vercel vercel bot temporarily deployed to Preview – docs September 23, 2025 00:55 Inactive
@vercel vercel bot temporarily deployed to Preview – docs September 23, 2025 01:30 Inactive
Comment on lines 54 to 57
const handleConfirmCancel = () => {
// Cancellation logic will be implemented later
setShowCancelModal(false);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

The subscription cancellation implementation is incomplete. The handleConfirmCancel function only closes the modal without actually canceling the subscription. This creates a misleading user experience where users might believe they've canceled their subscription when they haven't. Consider implementing the actual cancellation logic before shipping this feature, or clearly indicating to users that this functionality is coming soon if it must be shipped in this state.

Suggested change
const handleConfirmCancel = () => {
// Cancellation logic will be implemented later
setShowCancelModal(false);
};
const handleConfirmCancel = async () => {
try {
// Call the subscription cancellation API
await cancelUserSubscription();
// Show success message to user
toast.success("Your subscription has been successfully canceled");
// Close the modal
setShowCancelModal(false);
} catch (error) {
// Handle errors
console.error("Failed to cancel subscription:", error);
toast.error("Failed to cancel your subscription. Please try again or contact support.");
}
};

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
apps/web/client/src/app/_components/top-bar/github.tsx (1)

21-51: Avoid “undefined”/NaN UI on GitHub errors; add ok-checks, type guards, timeout, and cleanup.

GitHub returns JSON errors on rate limits; without response.ok checks, stargazers_count can be undefined and render “undefined”. Add guards and abort to prevent hangs and set safe fallbacks.

Apply this diff:

 useEffect(() => {
-    const fetchStats = async () => {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 5000);
+    const fetchStats = async () => {
         try {
             // Stars
-            const repoResponse = await fetch('https://api.github.com/repos/onlook-dev/onlook');
-            const repoData = await repoResponse.json();
-            setRaw(repoData.stargazers_count);
-            setFormatted(formatStarCount(repoData.stargazers_count));
+            const repoResponse = await fetch('https://api.github.com/repos/onlook-dev/onlook', {
+                signal: controller.signal,
+                headers: { Accept: 'application/vnd.github+json' },
+            });
+            if (!repoResponse.ok) throw new Error(`GitHub repo fetch failed: ${repoResponse.status}`);
+            const repoData = await repoResponse.json();
+            const stars =
+                typeof repoData?.stargazers_count === 'number'
+                    ? repoData.stargazers_count
+                    : DEFAULT_STAR_COUNT;
+            setRaw(stars);
+            setFormatted(formatStarCount(stars));
 
             // Contributors (use the Link header for pagination)
-            const contribResponse = await fetch('https://api.github.com/repos/onlook-dev/onlook/contributors?per_page=1&anon=true');
+            const contribResponse = await fetch(
+                'https://api.github.com/repos/onlook-dev/onlook/contributors?per_page=1&anon=true',
+                { signal: controller.signal, headers: { Accept: 'application/vnd.github+json' } }
+            );
+            if (!contribResponse.ok) throw new Error(`GitHub contributors fetch failed: ${contribResponse.status}`);
             const linkHeader = contribResponse.headers.get('Link');
             if (linkHeader) {
-                const match = linkHeader.match(/&page=(\d+)>; rel="last"/);
-                if (match) {
-                    setContributors(Number(match[1]));
-                }
+                const match = linkHeader.match(/[?&]page=(\d+)>; rel="last"/);
+                if (match) setContributors(Number(match[1]));
+                else setContributors(DEFAULT_CONTRIBUTORS_COUNT);
             } else {
                 // fallback: count the single returned contributor
                 const contribData = await contribResponse.json();
                 setContributors(Array.isArray(contribData) ? contribData.length : DEFAULT_CONTRIBUTORS_COUNT);
             }
         } catch (error) {
             console.error('Failed to fetch GitHub stats:', error);
             setRaw(DEFAULT_STAR_COUNT);
             setFormatted(formatStarCount(DEFAULT_STAR_COUNT));
             setContributors(DEFAULT_CONTRIBUTORS_COUNT);
+        } finally {
+            clearTimeout(timeoutId);
         }
     };
     fetchStats();
-}, []);
+    return () => controller.abort();
+}, []);
🧹 Nitpick comments (8)
apps/web/client/src/app/_components/top-bar/github.tsx (2)

9-14: Optional: format millions as “M”.

Keeps display compact as stars grow.

 const formatStarCount = (count: number): string => {
-    if (count >= 1000) {
+    if (count >= 1_000_000) {
+        return `${(count / 1_000_000).toFixed(1)}M`.replace('.0M', 'M');
+    }
+    if (count >= 1000) {
         return `${(count / 1000).toFixed(1)}k`.replace('.0k', 'k');
     }
     return count.toString();
 };

21-51: Consider moving GitHub fetches server‑side with caching.

Expose a tiny API route (revalidate ~1h) and fetch from the client to avoid browser rate limits and improve resilience.

packages/db/src/schema/subscription/usage.ts (2)

12-12: onUpdate: 'cascade' is likely unnecessary for UUID PKs.

Primary keys for users shouldn’t change. Dropping onUpdate reduces surprise without losing safety.

Apply:

- userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),

15-16: Nit: avoid reserved-ish column names.

Consider renaming timestamp to occurredAt/recordedAt to reduce confusion with the SQL type name.

packages/db/src/schema/subscription/rate-limits.ts (2)

12-15: onUpdate: 'cascade' is unusual here.

IDs for users/subscriptions shouldn’t change. Safe to drop the onUpdate to reduce implicit data movement.

- .references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ .references(() => users.id, { onDelete: 'cascade' }),
...
- .references(() => subscriptions.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ .references(() => subscriptions.id, { onDelete: 'cascade' }),

41-43: Optional index to support subscription-scoped queries.

If you frequently fetch limits by subscription/time, add a covering index.

 export const rateLimits = pgTable('rate_limits', {
   ...
 }, (table) => [
   index('rate_limits_user_time_idx').on(table.userId, table.startedAt, table.endedAt),
+  index('rate_limits_subscription_time_idx').on(table.subscriptionId, table.startedAt, table.endedAt),
 ])
packages/db/src/schema/subscription/subscription.ts (2)

36-36: Make scheduledPriceId nullable on delete.

If a Price is removed/archived, the scheduled reference should clear instead of blocking deletion.

- scheduledPriceId: uuid('scheduled_price_id').references(() => prices.id),
+ scheduledPriceId: uuid('scheduled_price_id').references(() => prices.id, { onDelete: 'set null', onUpdate: 'cascade' }),

15-17: Minor: onUpdate cascades are redundant.

UUID PKs aren’t expected to change; consider dropping onUpdate for productId/priceId for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df4daab and 2bf01f1.

📒 Files selected for processing (7)
  • apps/web/client/src/app/_components/top-bar/github.tsx (1 hunks)
  • apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx (1 hunks)
  • apps/web/client/src/server/api/routers/user/user.ts (2 hunks)
  • packages/db/src/schema/domain/deployment.ts (1 hunks)
  • packages/db/src/schema/subscription/rate-limits.ts (1 hunks)
  • packages/db/src/schema/subscription/subscription.ts (1 hunks)
  • packages/db/src/schema/subscription/usage.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/client/src/components/ui/settings-modal/user-delete-section.tsx
  • apps/web/client/src/server/api/routers/user/user.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • packages/db/src/schema/subscription/usage.ts
  • packages/db/src/schema/domain/deployment.ts
  • apps/web/client/src/app/_components/top-bar/github.tsx
  • packages/db/src/schema/subscription/rate-limits.ts
  • packages/db/src/schema/subscription/subscription.ts
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • packages/db/src/schema/subscription/usage.ts
  • packages/db/src/schema/domain/deployment.ts
  • apps/web/client/src/app/_components/top-bar/github.tsx
  • packages/db/src/schema/subscription/rate-limits.ts
  • packages/db/src/schema/subscription/subscription.ts
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/_components/top-bar/github.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/_components/top-bar/github.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/_components/top-bar/github.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/_components/top-bar/github.tsx
🧬 Code graph analysis (4)
packages/db/src/schema/subscription/usage.ts (1)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
packages/db/src/schema/domain/deployment.ts (1)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
packages/db/src/schema/subscription/rate-limits.ts (2)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
packages/db/src/schema/subscription/subscription.ts (1)
  • subscriptions (11-38)
packages/db/src/schema/subscription/subscription.ts (3)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
packages/db/src/schema/subscription/price.ts (1)
  • prices (8-22)
🔇 Additional comments (5)
apps/web/client/src/app/_components/top-bar/github.tsx (1)

6-6: LGTM: refreshed fallback star count.

Good to keep the initial UI state current.

packages/db/src/schema/subscription/rate-limits.ts (1)

12-15: Cascades on userId/subscriptionId look correct for lifecycle.

Rate limits should disappear with the parent user/subscription. LGTM.

Before enabling account deletion in prod, ensure Stripe subscriptions are cancelled first to avoid orphaned live billing. Do we already do this in the delete flow?

packages/db/src/schema/subscription/subscription.ts (1)

15-17: Change productId/priceId FK onDelete to RESTRICT; keep userId CASCADE

Products/prices are rarely deleted — cascading would remove subscriptions. Keep CASCADE for userId so account deletions still remove a user’s subscriptions.

File: packages/db/src/schema/subscription/subscription.ts (lines 15–17)

- userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
- productId: uuid('product_id').notNull().references(() => products.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
- priceId: uuid('price_id').notNull().references(() => prices.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ productId: uuid('product_id').notNull().references(() => products.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
+ priceId: uuid('price_id').notNull().references(() => prices.id, { onDelete: 'restrict', onUpdate: 'cascade' }),

Operational: ensure account-deletion flow cancels Stripe subscriptions before deleting the user to avoid live Stripe subs with missing DB rows.

Verify: quick scan found no code deleting products/prices — re-run to confirm:

rg -nP "delete\s+from\s+products|delete\s+from\s+prices|\bdb\.\s*delete\s*\(\s*products|\bdb\.\s*delete\s*\(\s*prices" -C2
packages/db/src/schema/domain/deployment.ts (1)

14-14: Avoid hard-deleting deployments — make requestedBy nullable and use ON DELETE SET NULL.

File: packages/db/src/schema/domain/deployment.ts (around line 14)

Deployments/build logs are historical artifacts; cascade deletes will permanently remove them.

- requestedBy: uuid('requested_by').references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(),
+ requestedBy: uuid('requested_by').references(() => users.id, { onDelete: 'set null', onUpdate: 'cascade' }),

Do product requirements allow losing deployment history when an account is deleted? If not, adopt SET NULL or reassign to a "System" user.

packages/db/src/schema/subscription/usage.ts (1)

12-12: Confirm data-retention/audit policy — cascading user deletes will remove many child records

Deleting a user will hard-delete child rows; FK cascades to users.id are present in:

  • packages/db/src/schema/user/user-canvas.ts
  • packages/db/src/schema/user/user-project.ts
  • packages/db/src/schema/subscription/usage.ts
  • packages/db/src/schema/subscription/rate-limits.ts
  • packages/db/src/schema/subscription/subscription.ts
  • packages/db/src/schema/domain/deployment.ts
  • packages/db/src/schema/project/invitation.ts

If you need billing/dispute/analytics retention, switch to soft-delete or archive records before user delete, or change FK onDelete to RESTRICT/SET NULL. FK cascades execute as the table owner and bypass RLS.

@vercel vercel bot temporarily deployed to Preview – docs September 23, 2025 01:41 Inactive
await localforage.setItem(LAST_SIGN_IN_METHOD_KEY, method);
await login(method);
} catch (error) {
toast.error('Error signing in with password');
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using a generic error message instead of 'Error signing in with password' since this flow may not always involve a password (e.g., OAuth).

Suggested change
toast.error('Error signing in with password');
toast.error('Error signing in');

Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
apps/web/client/src/app/login/actions.tsx (1)

13-15: Do not trust request Origin for OAuth redirectTo (open‑redirect risk).

Using the client‑supplied Origin header to build the OAuth callback enables host/header spoofing and can redirect users to attacker‑controlled domains. Always derive the callback from a trusted base URL.

Apply this diff:

-    const origin = (await headers()).get('origin') ?? env.NEXT_PUBLIC_SITE_URL;
-    const redirectTo = `${origin}${Routes.AUTH_CALLBACK}`;
+    const redirectTo = new URL(Routes.AUTH_CALLBACK, env.NEXT_PUBLIC_SITE_URL).toString();
🧹 Nitpick comments (6)
packages/db/src/seed/db.ts (4)

174-248: DRY up message seed creation with a small factory.

Five seeds share the same shape. A helper reduces repetition and centralizes defaults (incl. usage).

Example:

function makeMessage(overrides: Partial<Message>): Message {
  return {
    id: uuidv4(),
    conversationId: conversation0.id,
    role: 'user',
    content: '',
    commitOid: null,
    createdAt: new Date(),
    applied: false,
    context: contexts,
    checkpoints: [],
    parts: [],
    snapshots: null,
    usage: null,
    ...overrides,
  } as Message;
}

349-349: Duplicate deletion of userProjects in resetDb.

await tx.delete(userProjects); appears at Line 338 and again here. Drop the second call.

Apply:

-        await tx.delete(userProjects);
         await tx.delete(projects);

52-54: Stripe customer ID mismatch between user and subscription.

user0.stripeCustomerId is null while subscription0.stripeCustomerId is set. If future “Manage billing” flows rely on user.stripeCustomerId, seeds may not reflect real paths.

Option: define and reuse a single seeded constant for both.

const STRIPE_CUSTOMER_ID = 'cus_1234567890';
// user0.stripeCustomerId: STRIPE_CUSTOMER_ID
// subscription0.stripeCustomerId: STRIPE_CUSTOMER_ID

Also applies to: 272-272


32-32: Avoid importing into another package’s src via alias.

import { db } from '@onlook/db/src/client' relies on aliasing into /src. Prefer the local module entry or a relative path to avoid packaging/TSConfig coupling.

apps/web/client/src/app/login/actions.tsx (2)

8-8: Remove now‑unused headers() import.

After switching to a trusted base URL, this import is unused and will trip linting.

-import { headers } from 'next/headers';

33-35: Log OAuth errors before redirecting to aid debugging.

We swallow details on the error path. Add a server‑side log (or your structured logger) before redirect.

-    if (error) {
-        redirect('/error');
-    }
+    if (error) {
+        console.error('OAuth sign-in error', { provider, error });
+        redirect('/error');
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf01f1 and 291233a.

📒 Files selected for processing (4)
  • apps/web/client/src/app/auth/auth-context.tsx (2 hunks)
  • apps/web/client/src/app/login/actions.tsx (1 hunks)
  • apps/web/client/src/server/api/routers/user/user.ts (2 hunks)
  • packages/db/src/seed/db.ts (5 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/login/actions.tsx
  • apps/web/client/src/app/auth/auth-context.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/login/actions.tsx
  • apps/web/client/src/server/api/routers/user/user.ts
  • apps/web/client/src/app/auth/auth-context.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/login/actions.tsx
  • apps/web/client/src/app/auth/auth-context.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/login/actions.tsx
  • packages/db/src/seed/db.ts
  • apps/web/client/src/server/api/routers/user/user.ts
  • apps/web/client/src/app/auth/auth-context.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/login/actions.tsx
  • apps/web/client/src/app/auth/auth-context.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/app/login/actions.tsx
  • packages/db/src/seed/db.ts
  • apps/web/client/src/server/api/routers/user/user.ts
  • apps/web/client/src/app/auth/auth-context.tsx
apps/web/client/src/server/api/routers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/server/api/routers/**/*.ts: Place tRPC routers under apps/web/client/src/server/api/routers/**
Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod
Return plain objects/arrays; rely on SuperJSON for serialization in tRPC procedures

apps/web/client/src/server/api/routers/**/*.ts: Place tRPC routers under src/server/api/routers/**
Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod
Return plain objects/arrays; rely on SuperJSON for serialization

Files:

  • apps/web/client/src/server/api/routers/user/user.ts
🧠 Learnings (4)
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/web/client/src/server/api/routers/user/user.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/web/client/src/server/api/routers/user/user.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/**/*.{ts,tsx} : Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Applied to files:

  • apps/web/client/src/app/auth/auth-context.tsx
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Avoid hardcoded user-facing text; use next-intl messages/hooks

Applied to files:

  • apps/web/client/src/app/auth/auth-context.tsx
🧬 Code graph analysis (2)
apps/web/client/src/server/api/routers/user/user.ts (2)
apps/web/client/src/server/api/trpc.ts (1)
  • protectedProcedure (131-150)
packages/db/src/schema/supabase/user.ts (1)
  • authUsers (5-10)
apps/web/client/src/app/auth/auth-context.tsx (3)
apps/web/client/src/utils/constants/index.ts (1)
  • LocalForageKeys (48-50)
apps/web/client/src/app/login/actions.tsx (2)
  • login (11-38)
  • devLogin (40-63)
packages/ui/src/components/sonner.tsx (1)
  • toast (19-19)
🔇 Additional comments (6)
packages/db/src/seed/db.ts (1)

186-186: Message.usage is nullable and consumers handle null — seed null is optional.

  • schema: packages/db/src/schema/chat/message.ts — usage: jsonb("usage") (no .notNull()/.default() → nullable).
  • mapper: packages/db/src/mappers/chat/message.ts — usage: message.metadata?.usage ?? null.
  • UI: apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/error-message.tsx — Usage | null.

Explicit usage: null in seeds is redundant (omitting the field will result in NULL); safe to remove or leave as-is.

apps/web/client/src/server/api/routers/user/user.ts (2)

98-100: Replace raw DB delete with Admin delete + app-data cleanup + explicit result; ensure ctx preserves supabase (dup of earlier note)

Call ctx.supabase.auth.admin.deleteUser first, then clean up app data (public.users) to trigger your CASCADEs. Handle errors and return a plain object. Also fix protectedProcedure to spread the original ctx so supabase is present.

Apply to this router:

-    delete: protectedProcedure.mutation(async ({ ctx }) => {
-        await ctx.db.delete(authUsers).where(eq(authUsers.id, ctx.user.id));
-    }),
+    delete: protectedProcedure.mutation(async ({ ctx }) => {
+        try {
+            // 1) Remove auth user and sessions via Supabase Admin API
+            await ctx.supabase.auth.admin.deleteUser(ctx.user.id);
+        } catch (err) {
+            throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to delete auth user' });
+        }
+
+        // 2) Cleanup application data; CASCADEs from `users` will remove dependents
+        await ctx.db.transaction(async (tx) => {
+            await tx.delete(users).where(eq(users.id, ctx.user.id));
+        });
+
+        return { success: true } as const;
+    }),

And fix apps/web/client/src/server/api/trpc.ts so ctx.supabase survives protectedProcedure:

 return next({
-    ctx: {
-        // infers the `session` as non-nullable
-        user: ctx.user as SetRequiredDeep<User, 'email'>,
-        db: ctx.db,
-    },
+    ctx: {
+        ...ctx,
+        // infers the `session` as non-nullable
+        user: ctx.user as SetRequiredDeep<User, 'email'>,
+    },
 });

Verify FK cascades originate from public.users (not auth.users) so step (2) wipes dependents:

#!/bin/bash
# Inspect drizzle schema for CASCADEs hanging off `users`
rg -nP -C2 --type=ts "references\(\s*\(\)\s*=>\s*users\.id" packages/db/src/schema | sed -n '1,200p'
rg -nP -C2 --type=ts "onDelete\(['\"]cascade['\"]\)" packages/db/src/schema | sed -n '1,200p'

3-3: Prefer Supabase Admin API over direct auth.users deletes; remove unused import

Directly deleting from auth.users can bypass GoTrue cleanup (sessions, identities, factors). Use the Admin API and drop this import.

-import { authUsers, fromDbUser, userInsertSchema, users, type User } from '@onlook/db';
+import { fromDbUser, userInsertSchema, users, type User } from '@onlook/db';
+import { TRPCError } from '@trpc/server';
apps/web/client/src/app/login/actions.tsx (1)

58-61: Confirm dev-only and error exposure

  • devLogin is only defined in apps/web/client/src/app/login/actions.tsx and its sole call site is handleDevLogin in apps/web/client/src/app/auth/auth-context.tsx — the server-side guard (process.env.NODE_ENV !== 'development') prevents the actual supabase sign-in from running in non-development.
  • However, auth-context.handleDevLogin catches thrown errors and passes error.message into toast.description, so any message thrown by devLogin will appear in the UI when handleDevLogin is invoked.

Action: either (A) keep as-is if you accept developer-visible error messages in dev flows only, or (B) avoid passing raw error.message to the toast (use a generic message) if you want to ensure no downstream UI ever displays upstream error texts even in development.

Location: apps/web/client/src/app/login/actions.tsx (devLogin) and apps/web/client/src/app/auth/auth-context.tsx (handleDevLogin).

apps/web/client/src/app/auth/auth-context.tsx (2)

5-5: LGTM: toast import in a client component

Appropriate for a client-only context.


37-52: Remove rethrow; internationalize OAuth toast; prefer alias import

  • Replace catch block to avoid throwing in a UI handler and use next-intl for an OAuth-specific message:
 } catch (error) {
-    toast.error('Error signing in with password');
-    console.error('Error signing in with password:', error);
-    throw new Error('Error signing in with password');
+    toast.error(t('auth.signIn.oauthErrorTitle'), {
+      description: error instanceof Error ? error.message : t('common.tryAgain'),
+    });
+    console.error('auth.signIn.oauthError', error);
+    return;
 } finally {
  • Add useTranslations in the component:
import { useTranslations } from 'next-intl';
// ...
export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const t = useTranslations();
  // ...
}
  • Prefer alias import for actions (file: apps/web/client/src/app/auth/auth-context.tsx):

Replace

import { devLogin, login } from '../login/actions';

with

import { devLogin, login } from '@/app/login/actions';
  • Verification: I confirmed the relative import in apps/web/client/src/app/auth/auth-context.tsx; repository-wide hardcoded toast.error occurrences were not fully verified in this run — execute these locally to finish verification:

rg -n -C1 "toast.error(['"][^'\"]+['"]" apps/web/client/src
rg -n -C1 "from ['"]../login/actions['"]" apps/web/client/src

Comment on lines 54 to 70
const handleDevLogin = async (returnUrl: string | null) => {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
setTimeout(() => {
try {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
} catch (error) {
toast.error('Error signing in with password', {
description: error instanceof Error ? error.message : 'Please try again.',
});
console.error('Error signing in with password:', error);
throw new Error('Error signing in with password');
} finally {
setSigningInMethod(null);
}, 5000);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Align dev login error handling with OAuth; avoid rethrow and hardcoded text

  • Don’t rethrow after toasting; return instead.
  • Use next-intl; keep the descriptive message.
  • Unify console tags.

Apply within this block:

 } catch (error) {
-    toast.error('Error signing in with password', {
-        description: error instanceof Error ? error.message : 'Please try again.',
-    });
-    console.error('Error signing in with password:', error);
-    throw new Error('Error signing in with password');
+    toast.error(t('auth.signIn.passwordErrorTitle'), {
+        description: error instanceof Error ? error.message : t('common.tryAgain'),
+    });
+    console.error('auth.signIn.devError', error);
+    return;
 } finally {
📝 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 handleDevLogin = async (returnUrl: string | null) => {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
setTimeout(() => {
try {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
} catch (error) {
toast.error('Error signing in with password', {
description: error instanceof Error ? error.message : 'Please try again.',
});
console.error('Error signing in with password:', error);
throw new Error('Error signing in with password');
} finally {
setSigningInMethod(null);
}, 5000);
}
}
const handleDevLogin = async (returnUrl: string | null) => {
try {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
} catch (error) {
toast.error(t('auth.signIn.passwordErrorTitle'), {
description: error instanceof Error ? error.message : t('common.tryAgain'),
});
console.error('auth.signIn.devError', error);
return;
} finally {
setSigningInMethod(null);
}
}
🤖 Prompt for AI Agents
In apps/web/client/src/app/auth/auth-context.tsx around lines 54 to 70, the dev
login catch block currently rethrows, uses a hardcoded English error title, and
prints a console message inconsistent with OAuth handlers; update it to mirror
the OAuth error handling by (1) replacing the hardcoded toast title with
next-intl usage (use intl.formatMessage for the title) while keeping the
descriptive message in the description, (2) remove the throw and simply return
after showing the toast, and (3) standardize the console error tag to match the
OAuth handlers (e.g., use the same prefix/string used elsewhere); preserve the
finally block that clears signingInMethod.

@vercel vercel bot temporarily deployed to Preview – docs September 23, 2025 02:06 Inactive
@vercel vercel bot temporarily deployed to Preview – docs September 23, 2025 04:25 Inactive
@Kitenite Kitenite merged commit 1743e63 into main Sep 23, 2025
5 of 7 checks passed
@Kitenite Kitenite deleted the settings-subscription branch September 23, 2025 04:27
@coderabbitai coderabbitai bot mentioned this pull request Oct 12, 2025
5 tasks
@coderabbitai coderabbitai bot mentioned this pull request Oct 23, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants